settings_chat_links.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "settings/business/settings_chat_links.h"
  8. #include "api/api_chat_links.h"
  9. #include "apiwrap.h"
  10. #include "base/event_filter.h"
  11. #include "boxes/peers/edit_peer_invite_link.h"
  12. #include "boxes/peers/edit_peer_invite_links.h"
  13. #include "boxes/premium_preview_box.h"
  14. #include "boxes/peer_list_box.h"
  15. #include "chat_helpers/emoji_suggestions_widget.h"
  16. #include "chat_helpers/message_field.h"
  17. #include "chat_helpers/tabbed_panel.h"
  18. #include "chat_helpers/tabbed_selector.h"
  19. #include "core/application.h"
  20. #include "core/ui_integration.h"
  21. #include "core/core_settings.h"
  22. #include "data/stickers/data_custom_emoji.h"
  23. #include "data/data_document.h"
  24. #include "data/data_user.h"
  25. #include "lang/lang_keys.h"
  26. #include "main/main_app_config.h"
  27. #include "main/main_session.h"
  28. #include "settings/business/settings_recipients_helper.h"
  29. #include "ui/boxes/confirm_box.h"
  30. #include "ui/controls/emoji_button.h"
  31. #include "ui/text/text_utilities.h"
  32. #include "ui/widgets/buttons.h"
  33. #include "ui/widgets/fields/input_field.h"
  34. #include "ui/widgets/popup_menu.h"
  35. #include "ui/wrap/slide_wrap.h"
  36. #include "ui/wrap/vertical_layout.h"
  37. #include "ui/painter.h"
  38. #include "ui/rect.h"
  39. #include "ui/vertical_list.h"
  40. #include "window/window_session_controller.h"
  41. #include "styles/style_chat.h"
  42. #include "styles/style_chat_helpers.h"
  43. #include "styles/style_info.h"
  44. #include "styles/style_layers.h"
  45. #include "styles/style_menu_icons.h"
  46. #include "styles/style_settings.h"
  47. #include <QtGui/QGuiApplication>
  48. namespace Settings {
  49. namespace {
  50. constexpr auto kChangesDebounceTimeout = crl::time(1000);
  51. using ChatLinkData = Api::ChatLink;
  52. class ChatLinks final : public BusinessSection<ChatLinks> {
  53. public:
  54. ChatLinks(
  55. QWidget *parent,
  56. not_null<Window::SessionController*> controller);
  57. ~ChatLinks();
  58. [[nodiscard]] rpl::producer<QString> title() override;
  59. const Ui::RoundRect *bottomSkipRounding() const override {
  60. return &_bottomSkipRounding;
  61. }
  62. private:
  63. void setupContent(not_null<Window::SessionController*> controller);
  64. Ui::RoundRect _bottomSkipRounding;
  65. };
  66. struct ChatLinkAction {
  67. enum class Type {
  68. Copy,
  69. Share,
  70. Rename,
  71. Delete,
  72. };
  73. QString link;
  74. Type type = Type::Copy;
  75. };
  76. class Row;
  77. class RowDelegate {
  78. public:
  79. virtual not_null<Main::Session*> rowSession() = 0;
  80. virtual void rowUpdateRow(not_null<Row*> row) = 0;
  81. virtual void rowPaintIcon(
  82. QPainter &p,
  83. int x,
  84. int y,
  85. int size) = 0;
  86. };
  87. class Row final : public PeerListRow {
  88. public:
  89. Row(not_null<RowDelegate*> delegate, const ChatLinkData &data);
  90. void update(const ChatLinkData &data);
  91. [[nodiscard]] ChatLinkData data() const;
  92. QString generateName() override;
  93. QString generateShortName() override;
  94. PaintRoundImageCallback generatePaintUserpicCallback(
  95. bool forceRound) override;
  96. QSize rightActionSize() const override;
  97. QMargins rightActionMargins() const override;
  98. void rightActionPaint(
  99. Painter &p,
  100. int x,
  101. int y,
  102. int outerWidth,
  103. bool selected,
  104. bool actionSelected) override;
  105. bool rightActionDisabled() const override {
  106. return true;
  107. }
  108. void paintStatusText(
  109. Painter &p,
  110. const style::PeerListItem &st,
  111. int x,
  112. int y,
  113. int availableWidth,
  114. int outerWidth,
  115. bool selected) override;
  116. private:
  117. void updateStatus(const ChatLinkData &data);
  118. const not_null<RowDelegate*> _delegate;
  119. ChatLinkData _data;
  120. Ui::Text::String _status;
  121. Ui::Text::String _clicks;
  122. };
  123. [[nodiscard]] uint64 ComputeRowId(const ChatLinkData &data) {
  124. return UniqueRowIdFromString(data.link);
  125. }
  126. [[nodiscard]] QString ComputeClicks(const ChatLinkData &link) {
  127. return link.clicks
  128. ? tr::lng_chat_links_clicks(tr::now, lt_count, link.clicks)
  129. : tr::lng_chat_links_no_clicks(tr::now);
  130. }
  131. Row::Row(not_null<RowDelegate*> delegate, const ChatLinkData &data)
  132. : PeerListRow(ComputeRowId(data))
  133. , _delegate(delegate)
  134. , _data(data) {
  135. setCustomStatus(QString());
  136. updateStatus(data);
  137. }
  138. void Row::updateStatus(const ChatLinkData &data) {
  139. const auto context = Core::TextContext({
  140. .session = _delegate->rowSession(),
  141. .repaint = [=] { _delegate->rowUpdateRow(this); },
  142. });
  143. _status.setMarkedText(
  144. st::messageTextStyle,
  145. data.message,
  146. kMarkupTextOptions,
  147. context);
  148. _clicks.setText(st::messageTextStyle, ComputeClicks(data));
  149. }
  150. void Row::update(const ChatLinkData &data) {
  151. _data = data;
  152. updateStatus(data);
  153. refreshName(st::inviteLinkList.item);
  154. _delegate->rowUpdateRow(this);
  155. }
  156. ChatLinkData Row::data() const {
  157. return _data;
  158. }
  159. QString Row::generateName() {
  160. if (!_data.title.isEmpty()) {
  161. return _data.title;
  162. }
  163. auto result = _data.link;
  164. return result.replace(
  165. u"https://"_q,
  166. QString()
  167. );
  168. }
  169. QString Row::generateShortName() {
  170. return generateName();
  171. }
  172. PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
  173. return [=](
  174. QPainter &p,
  175. int x,
  176. int y,
  177. int outerWidth,
  178. int size) {
  179. _delegate->rowPaintIcon(p, x, y, size);
  180. };
  181. }
  182. QSize Row::rightActionSize() const {
  183. return QSize(
  184. _clicks.maxWidth(),
  185. st::inviteLinkThreeDotsIcon.height());
  186. }
  187. QMargins Row::rightActionMargins() const {
  188. return QMargins(
  189. 0,
  190. (st::inviteLinkList.item.height - rightActionSize().height()) / 2,
  191. st::inviteLinkThreeDotsSkip,
  192. 0);
  193. }
  194. void Row::rightActionPaint(
  195. Painter &p,
  196. int x,
  197. int y,
  198. int outerWidth,
  199. bool selected,
  200. bool actionSelected) {
  201. p.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg);
  202. _clicks.draw(p, x, y, outerWidth);
  203. }
  204. void Row::paintStatusText(
  205. Painter &p,
  206. const style::PeerListItem &st,
  207. int x,
  208. int y,
  209. int availableWidth,
  210. int outerWidth,
  211. bool selected) {
  212. p.setPen(selected ? st.statusFgOver : st.statusFg);
  213. _status.draw(p, {
  214. .position = { x, y },
  215. .outerWidth = outerWidth,
  216. .availableWidth = availableWidth,
  217. .palette = &st::defaultTextPalette,
  218. .spoiler = Ui::Text::DefaultSpoilerCache(),
  219. .now = crl::now(),
  220. .elisionLines = 1,
  221. });
  222. }
  223. class LinksController final
  224. : public PeerListController
  225. , public RowDelegate
  226. , public base::has_weak_ptr {
  227. public:
  228. explicit LinksController(not_null<Window::SessionController*> window);
  229. [[nodiscard]] rpl::producer<int> fullCountValue() const {
  230. return _count.value();
  231. }
  232. void prepare() override;
  233. void rowClicked(not_null<PeerListRow*> row) override;
  234. void rowRightActionClicked(not_null<PeerListRow*> row) override;
  235. base::unique_qptr<Ui::PopupMenu> rowContextMenu(
  236. QWidget *parent,
  237. not_null<PeerListRow*> row) override;
  238. Main::Session &session() const override;
  239. not_null<Main::Session*> rowSession() override;
  240. void rowUpdateRow(not_null<Row*> row) override;
  241. void rowPaintIcon(
  242. QPainter &p,
  243. int x,
  244. int y,
  245. int size) override;
  246. private:
  247. void appendRow(const ChatLinkData &data);
  248. void prependRow(const ChatLinkData &data);
  249. void updateRow(const ChatLinkData &data);
  250. bool removeRow(const QString &link);
  251. void showRowMenu(
  252. not_null<PeerListRow*> row,
  253. bool highlightRow);
  254. [[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
  255. QWidget *parent,
  256. not_null<PeerListRow*> row);
  257. const not_null<Window::SessionController*> _window;
  258. const not_null<Main::Session*> _session;
  259. rpl::variable<int> _count;
  260. base::unique_qptr<Ui::PopupMenu> _menu;
  261. QImage _icon;
  262. rpl::lifetime _lifetime;
  263. };
  264. struct LinksList {
  265. not_null<Ui::RpWidget*> widget;
  266. not_null<LinksController*> controller;
  267. };
  268. LinksList AddLinksList(
  269. not_null<Window::SessionController*> window,
  270. not_null<Ui::VerticalLayout*> container) {
  271. auto &lifetime = container->lifetime();
  272. const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
  273. window->uiShow());
  274. const auto controller = lifetime.make_state<LinksController>(window);
  275. controller->setStyleOverrides(&st::inviteLinkList);
  276. const auto content = container->add(object_ptr<PeerListContent>(
  277. container,
  278. controller));
  279. delegate->setContent(content);
  280. controller->setDelegate(delegate);
  281. return { content, controller };
  282. }
  283. void EditChatLinkBox(
  284. not_null<Ui::GenericBox*> box,
  285. not_null<Window::SessionController*> controller,
  286. ChatLinkData data,
  287. Fn<void(ChatLinkData, Fn<void()> close)> submit) {
  288. box->setTitle(data.link.isEmpty()
  289. ? tr::lng_chat_link_new_title()
  290. : tr::lng_chat_link_edit_title());
  291. box->setWidth(st::boxWideWidth);
  292. Ui::AddDividerText(
  293. box->verticalLayout(),
  294. tr::lng_chat_link_description());
  295. const auto peer = controller->session().user();
  296. const auto outer = box->getDelegate()->outerContainer();
  297. const auto field = box->addRow(
  298. object_ptr<Ui::InputField>(
  299. box.get(),
  300. st::settingsChatLinkField,
  301. Ui::InputField::Mode::MultiLine,
  302. tr::lng_chat_link_placeholder()));
  303. box->setFocusCallback([=] {
  304. field->setFocusFast();
  305. });
  306. Ui::AddDivider(box->verticalLayout());
  307. Ui::AddSkip(box->verticalLayout());
  308. const auto title = box->addRow(object_ptr<Ui::InputField>(
  309. box.get(),
  310. st::defaultInputField,
  311. tr::lng_chat_link_name(),
  312. data.title));
  313. const auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(
  314. field->parentWidget(),
  315. st::defaultComposeFiles.emoji);
  316. using Selector = ChatHelpers::TabbedSelector;
  317. auto &lifetime = box->lifetime();
  318. const auto emojiPanel = lifetime.make_state<ChatHelpers::TabbedPanel>(
  319. outer,
  320. controller,
  321. object_ptr<Selector>(
  322. nullptr,
  323. controller->uiShow(),
  324. Window::GifPauseReason::Layer,
  325. Selector::Mode::EmojiOnly));
  326. emojiPanel->setDesiredHeightValues(
  327. 1.,
  328. st::emojiPanMinHeight / 2,
  329. st::emojiPanMinHeight);
  330. emojiPanel->hide();
  331. emojiPanel->selector()->setCurrentPeer(peer);
  332. emojiPanel->selector()->emojiChosen(
  333. ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
  334. Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
  335. }, field->lifetime());
  336. emojiPanel->selector()->customEmojiChosen(
  337. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  338. Data::InsertCustomEmoji(field, data.document);
  339. }, field->lifetime());
  340. emojiToggle->installEventFilter(emojiPanel);
  341. emojiToggle->addClickHandler([=] {
  342. emojiPanel->toggleAnimated();
  343. });
  344. const auto allow = [](not_null<DocumentData*>) { return true; };
  345. InitMessageFieldHandlers(
  346. controller,
  347. field,
  348. Window::GifPauseReason::Layer,
  349. allow);
  350. Ui::Emoji::SuggestionsController::Init(
  351. outer,
  352. field,
  353. &controller->session(),
  354. { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
  355. field->setSubmitSettings(Core::App().settings().sendSubmitWay());
  356. field->setMaxHeight(st::defaultComposeFiles.caption.heightMax);
  357. const auto save = [=] {
  358. auto copy = data;
  359. copy.title = title->getLastText().trimmed();
  360. auto textWithTags = field->getTextWithAppliedMarkdown();
  361. copy.message = TextWithEntities{
  362. textWithTags.text,
  363. TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
  364. };
  365. submit(copy, crl::guard(box, [=] {
  366. box->closeBox();
  367. }));
  368. };
  369. const auto updateEmojiPanelGeometry = [=] {
  370. const auto parent = emojiPanel->parentWidget();
  371. const auto global = emojiToggle->mapToGlobal({ 0, 0 });
  372. const auto local = parent->mapFromGlobal(global);
  373. emojiPanel->moveBottomRight(
  374. local.y(),
  375. local.x() + emojiToggle->width() * 3);
  376. };
  377. const auto filterCallback = [=](not_null<QEvent*> event) {
  378. const auto type = event->type();
  379. if (type == QEvent::Move || type == QEvent::Resize) {
  380. // updateEmojiPanelGeometry uses not only container geometry, but
  381. // also container children geometries that will be updated later.
  382. crl::on_main(emojiPanel, updateEmojiPanelGeometry);
  383. }
  384. return base::EventFilterResult::Continue;
  385. };
  386. base::install_event_filter(emojiPanel, outer, filterCallback);
  387. field->submits(
  388. ) | rpl::start_with_next([=] {
  389. title->setFocus();
  390. }, field->lifetime());
  391. field->cancelled(
  392. ) | rpl::start_with_next([=] {
  393. box->closeBox();
  394. }, field->lifetime());
  395. title->submits(
  396. ) | rpl::start_with_next(save, title->lifetime());
  397. rpl::combine(
  398. box->sizeValue(),
  399. field->geometryValue()
  400. ) | rpl::start_with_next([=](QSize outer, QRect inner) {
  401. emojiToggle->moveToLeft(
  402. inner.x() + inner.width() - emojiToggle->width(),
  403. inner.y() + st::settingsChatLinkEmojiTop);
  404. emojiToggle->update();
  405. crl::on_main(emojiPanel, updateEmojiPanelGeometry);
  406. }, emojiToggle->lifetime());
  407. const auto initial = TextWithTags{
  408. data.message.text,
  409. TextUtilities::ConvertEntitiesToTextTags(data.message.entities)
  410. };
  411. field->setTextWithTags(initial, Ui::InputField::HistoryAction::Clear);
  412. auto cursor = field->textCursor();
  413. cursor.movePosition(QTextCursor::End);
  414. field->setTextCursor(cursor);
  415. const auto checkChangedTimer = lifetime.make_state<base::Timer>([=] {
  416. if (field->getTextWithAppliedMarkdown() == initial) {
  417. box->setCloseByOutsideClick(true);
  418. }
  419. });
  420. field->changes(
  421. ) | rpl::start_with_next([=] {
  422. checkChangedTimer->callOnce(kChangesDebounceTimeout);
  423. box->setCloseByOutsideClick(false);
  424. }, field->lifetime());
  425. box->addButton(tr::lng_settings_save(), save);
  426. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  427. }
  428. void EditChatLink(
  429. not_null<Window::SessionController*> window,
  430. not_null<Main::Session*> session,
  431. ChatLinkData data) {
  432. const auto submitting = std::make_shared<bool>();
  433. const auto submit = [=](ChatLinkData data, Fn<void()> close) {
  434. if (std::exchange(*submitting, true)) {
  435. return;
  436. }
  437. const auto done = crl::guard(window, [=](const auto&) {
  438. window->showToast(tr::lng_chat_link_saved(tr::now));
  439. close();
  440. });
  441. session->api().chatLinks().edit(
  442. data.link,
  443. data.title,
  444. data.message,
  445. done);
  446. };
  447. window->show(Box(
  448. EditChatLinkBox,
  449. window,
  450. data,
  451. crl::guard(window, submit)));
  452. }
  453. LinksController::LinksController(
  454. not_null<Window::SessionController*> window)
  455. : _window(window)
  456. , _session(&window->session()) {
  457. style::PaletteChanged(
  458. ) | rpl::start_with_next([=] {
  459. _icon = QImage();
  460. }, _lifetime);
  461. _session->api().chatLinks().updates(
  462. ) | rpl::start_with_next([=](const Api::ChatLinkUpdate &update) {
  463. if (!update.now) {
  464. if (removeRow(update.was)) {
  465. delegate()->peerListRefreshRows();
  466. }
  467. } else if (update.was.isEmpty()) {
  468. prependRow(*update.now);
  469. delegate()->peerListRefreshRows();
  470. } else {
  471. updateRow(*update.now);
  472. }
  473. }, _lifetime);
  474. }
  475. void LinksController::prepare() {
  476. auto &&list = _session->api().chatLinks().list()
  477. | ranges::views::reverse;
  478. for (const auto &link : list) {
  479. appendRow(link);
  480. }
  481. delegate()->peerListRefreshRows();
  482. }
  483. void LinksController::rowClicked(not_null<PeerListRow*> row) {
  484. showRowMenu(row, true);
  485. }
  486. void LinksController::showRowMenu(
  487. not_null<PeerListRow*> row,
  488. bool highlightRow) {
  489. delegate()->peerListShowRowMenu(row, highlightRow);
  490. }
  491. void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
  492. delegate()->peerListShowRowMenu(row, true);
  493. }
  494. base::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(
  495. QWidget *parent,
  496. not_null<PeerListRow*> row) {
  497. auto result = createRowContextMenu(parent, row);
  498. if (result) {
  499. // First clear _menu value, so that we don't check row positions yet.
  500. base::take(_menu);
  501. // Here unique_qptr is used like a shared pointer, where
  502. // not the last destroyed pointer destroys the object, but the first.
  503. _menu = base::unique_qptr<Ui::PopupMenu>(result.get());
  504. }
  505. return result;
  506. }
  507. base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
  508. QWidget *parent,
  509. not_null<PeerListRow*> row) {
  510. const auto real = static_cast<Row*>(row.get());
  511. const auto data = real->data();
  512. const auto link = data.link;
  513. auto result = base::make_unique_q<Ui::PopupMenu>(
  514. parent,
  515. st::popupMenuWithIcons);
  516. result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {
  517. QGuiApplication::clipboard()->setText(link);
  518. delegate()->peerListUiShow()->showToast(
  519. tr::lng_chat_link_copied(tr::now));
  520. }, &st::menuIconCopy);
  521. result->addAction(tr::lng_group_invite_context_share(tr::now), [=] {
  522. delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(
  523. _session,
  524. link,
  525. tr::lng_chat_link_copied(tr::now)));
  526. }, &st::menuIconShare);
  527. result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
  528. delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
  529. nullptr,
  530. link,
  531. tr::lng_chat_link_qr_title(),
  532. tr::lng_chat_link_qr_about()));
  533. }, &st::menuIconQrCode);
  534. result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
  535. EditChatLink(_window, _session, data);
  536. }, &st::menuIconEdit);
  537. result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {
  538. const auto sure = [=](Fn<void()> &&close) {
  539. _window->session().api().chatLinks().destroy(link, close);
  540. };
  541. _window->show(Ui::MakeConfirmBox({
  542. .text = tr::lng_chat_link_delete_sure(tr::now),
  543. .confirmed = sure,
  544. .confirmText = tr::lng_box_delete(tr::now),
  545. }));
  546. }, &st::menuIconDelete);
  547. return result;
  548. }
  549. Main::Session &LinksController::session() const {
  550. return *_session;
  551. }
  552. void LinksController::appendRow(const ChatLinkData &data) {
  553. delegate()->peerListAppendRow(std::make_unique<Row>(this, data));
  554. _count = _count.current() + 1;
  555. }
  556. void LinksController::prependRow(const ChatLinkData &data) {
  557. delegate()->peerListPrependRow(std::make_unique<Row>(this, data));
  558. _count = _count.current() + 1;
  559. }
  560. void LinksController::updateRow(const ChatLinkData &data) {
  561. if (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {
  562. const auto real = static_cast<Row*>(row);
  563. real->update(data);
  564. delegate()->peerListUpdateRow(row);
  565. }
  566. }
  567. bool LinksController::removeRow(const QString &link) {
  568. const auto id = UniqueRowIdFromString(link);
  569. if (const auto row = delegate()->peerListFindRow(id)) {
  570. delegate()->peerListRemoveRow(row);
  571. _count = std::max(_count.current() - 1, 0);
  572. return true;
  573. }
  574. return false;
  575. }
  576. not_null<Main::Session*> LinksController::rowSession() {
  577. return _session;
  578. }
  579. void LinksController::rowUpdateRow(not_null<Row*> row) {
  580. delegate()->peerListUpdateRow(row);
  581. }
  582. void LinksController::rowPaintIcon(
  583. QPainter &p,
  584. int x,
  585. int y,
  586. int size) {
  587. const auto skip = st::inviteLinkIconSkip;
  588. const auto inner = size - 2 * skip;
  589. const auto bg = &st::msgFile1Bg;
  590. if (_icon.isNull()) {
  591. _icon = QImage(
  592. QSize(inner, inner) * style::DevicePixelRatio(),
  593. QImage::Format_ARGB32_Premultiplied);
  594. _icon.fill(Qt::transparent);
  595. _icon.setDevicePixelRatio(style::DevicePixelRatio());
  596. auto p = QPainter(&_icon);
  597. p.setPen(Qt::NoPen);
  598. p.setBrush(*bg);
  599. {
  600. auto hq = PainterHighQualityEnabler(p);
  601. auto rect = QRect(0, 0, inner, inner);
  602. p.drawEllipse(rect);
  603. }
  604. st::inviteLinkIcon.paintInCenter(p, Rect(Size(inner)));
  605. }
  606. p.drawImage(x + skip, y + skip, _icon);
  607. }
  608. ChatLinks::ChatLinks(
  609. QWidget *parent,
  610. not_null<Window::SessionController*> controller)
  611. : BusinessSection(parent, controller)
  612. , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
  613. setupContent(controller);
  614. }
  615. ChatLinks::~ChatLinks() = default;
  616. rpl::producer<QString> ChatLinks::title() {
  617. return tr::lng_chat_links_title();
  618. }
  619. void ChatLinks::setupContent(
  620. not_null<Window::SessionController*> controller) {
  621. using namespace rpl::mappers;
  622. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  623. AddDividerTextWithLottie(content, {
  624. .lottie = u"chat_link"_q,
  625. .lottieSize = st::settingsCloudPasswordIconSize,
  626. .lottieMargins = st::peerAppearanceIconPadding,
  627. .showFinished = showFinishes() | rpl::take(1),
  628. .about = tr::lng_chat_links_about(Ui::Text::WithEntities),
  629. .aboutMargins = st::peerAppearanceCoverLabelMargin,
  630. });
  631. Ui::AddSkip(content);
  632. const auto limit = controller->session().appConfig().get<int>(
  633. u"business_chat_links_limit"_q,
  634. 100);
  635. const auto add = content->add(
  636. object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
  637. content,
  638. MakeCreateLinkButton(
  639. content,
  640. tr::lng_chat_links_create_link()))
  641. )->setDuration(0);
  642. const auto list = AddLinksList(controller, content);
  643. add->toggleOn(list.controller->fullCountValue() | rpl::map(_1 < limit));
  644. add->finishAnimating();
  645. add->entity()->setClickedCallback([=] {
  646. if (!controller->session().premium()) {
  647. ShowPremiumPreviewToBuy(
  648. controller,
  649. PremiumFeature::ChatLinks);
  650. return;
  651. }
  652. const auto submitting = std::make_shared<bool>();
  653. const auto submit = [=](ChatLinkData data, Fn<void()> close) {
  654. if (std::exchange(*submitting, true)) {
  655. return;
  656. }
  657. const auto done = [=](const auto&) {
  658. controller->showToast(tr::lng_chat_link_saved(tr::now));
  659. close();
  660. };
  661. controller->session().api().chatLinks().create(
  662. data.title,
  663. data.message,
  664. done);
  665. };
  666. controller->show(Box(
  667. EditChatLinkBox,
  668. controller,
  669. ChatLinkData(),
  670. crl::guard(this, submit)));
  671. });
  672. Ui::AddSkip(content);
  673. const auto self = controller->session().user();
  674. const auto username = self->username();
  675. const auto make = [&](std::vector<QString> links) {
  676. Expects(!links.empty());
  677. for (auto &link : links) {
  678. link = controller->session().createInternalLink(link);
  679. }
  680. return (links.size() > 1)
  681. ? tr::lng_chat_links_footer_both(
  682. tr::now,
  683. lt_username,
  684. Ui::Text::Link(links[0], "https://" + links[0]),
  685. lt_link,
  686. Ui::Text::Link(links[1], "https://" + links[1]),
  687. Ui::Text::WithEntities)
  688. : Ui::Text::Link(links[0], "https://" + links[0]);
  689. };
  690. auto links = !username.isEmpty()
  691. ? make({ username, '+' + self->phone() })
  692. : make({ '+' + self->phone() });
  693. auto label = object_ptr<Ui::FlatLabel>(
  694. content,
  695. tr::lng_chat_links_footer(
  696. lt_links,
  697. rpl::single(std::move(links)),
  698. Ui::Text::WithEntities),
  699. st::boxDividerLabel);
  700. label->setClickHandlerFilter([=](ClickHandlerPtr handler, auto) {
  701. QGuiApplication::clipboard()->setText(handler->url());
  702. controller->showToast(tr::lng_chat_link_copied(tr::now));
  703. return false;
  704. });
  705. content->add(object_ptr<Ui::DividerLabel>(
  706. content,
  707. std::move(label),
  708. st::settingsChatbotsBottomTextMargin,
  709. RectPart::Top));
  710. Ui::ResizeFitChild(this, content);
  711. }
  712. } // namespace
  713. Type ChatLinksId() {
  714. return ChatLinks::Id();
  715. }
  716. } // namespace Settings