edit_peer_invite_link.cpp 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753
  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 "boxes/peers/edit_peer_invite_link.h"
  8. #include "api/api_invite_links.h"
  9. #include "apiwrap.h"
  10. #include "base/unixtime.h"
  11. #include "boxes/gift_premium_box.h"
  12. #include "boxes/peer_list_box.h"
  13. #include "boxes/peer_list_controllers.h"
  14. #include "boxes/share_box.h"
  15. #include "core/application.h"
  16. #include "core/ui_integration.h" // TextContext
  17. #include "data/components/credits.h"
  18. #include "data/data_changes.h"
  19. #include "data/data_channel.h"
  20. #include "data/data_forum_topic.h"
  21. #include "data/data_histories.h"
  22. #include "data/data_peer.h"
  23. #include "data/data_session.h"
  24. #include "data/data_user.h"
  25. #include "data/stickers/data_custom_emoji.h"
  26. #include "history/history.h"
  27. #include "history/history_item_helpers.h" // GetErrorForSending.
  28. #include "history/view/history_view_group_call_bar.h" // GenerateUserpics...
  29. #include "lang/lang_keys.h"
  30. #include "main/main_session.h"
  31. #include "qr/qr_generate.h"
  32. #include "settings/settings_credits_graphics.h"
  33. #include "ui/boxes/confirm_box.h"
  34. #include "ui/boxes/edit_invite_link.h"
  35. #include "ui/boxes/edit_invite_link_session.h"
  36. #include "ui/boxes/peer_qr_box.h"
  37. #include "ui/controls/invite_link_buttons.h"
  38. #include "ui/controls/invite_link_label.h"
  39. #include "ui/controls/userpic_button.h"
  40. #include "ui/painter.h"
  41. #include "ui/rect.h"
  42. #include "ui/text/format_values.h"
  43. #include "ui/text/text_utilities.h"
  44. #include "ui/toast/toast.h"
  45. #include "ui/vertical_list.h"
  46. #include "ui/widgets/popup_menu.h"
  47. #include "ui/wrap/padding_wrap.h"
  48. #include "ui/wrap/slide_wrap.h"
  49. #include "ui/wrap/vertical_layout.h"
  50. #include "window/window_controller.h"
  51. #include "window/window_session_controller.h"
  52. #include "styles/style_boxes.h"
  53. #include "styles/style_credits.h"
  54. #include "styles/style_dialogs.h"
  55. #include "styles/style_giveaway.h"
  56. #include "styles/style_info.h"
  57. #include "styles/style_layers.h" // st::boxDividerLabel.
  58. #include "styles/style_menu_icons.h"
  59. #include "styles/style_premium.h"
  60. #include <QtCore/QMimeData>
  61. #include <QtGui/QGuiApplication>
  62. #include <QtSvg/QSvgRenderer>
  63. namespace {
  64. constexpr auto kFirstPage = 20;
  65. constexpr auto kPerPage = 100;
  66. // constexpr auto kShareQrSize = 768;
  67. // constexpr auto kShareQrPadding = 16;
  68. using LinkData = Api::InviteLink;
  69. void ShowPeerInfoSync(not_null<PeerData*> peer) {
  70. // While a peer info is demanded by the left click
  71. // we can safely use activeWindow.
  72. if (const auto window = Core::App().activeWindow()) {
  73. if (const auto controller = window->sessionController()) {
  74. if (&controller->session() == &peer->session()) {
  75. controller->showPeerInfo(peer);
  76. }
  77. }
  78. }
  79. }
  80. class SubscriptionRow final : public PeerListRow {
  81. public:
  82. SubscriptionRow(
  83. not_null<PeerData*> peer,
  84. TimeId date,
  85. Data::PeerSubscription subscription);
  86. QSize rightActionSize() const override;
  87. QMargins rightActionMargins() const override;
  88. void rightActionPaint(
  89. Painter &p,
  90. int x,
  91. int y,
  92. int outerWidth,
  93. bool selected,
  94. bool actionSelected) override;
  95. private:
  96. std::optional<Settings::SubscriptionRightLabel> _rightLabel;
  97. };
  98. SubscriptionRow::SubscriptionRow(
  99. not_null<PeerData*> peer,
  100. TimeId date,
  101. Data::PeerSubscription subscription)
  102. : PeerListRow(peer) {
  103. if (subscription) {
  104. _rightLabel = Settings::PaintSubscriptionRightLabelCallback(
  105. &peer->session(),
  106. st::peerListBoxItem,
  107. subscription.credits);
  108. }
  109. setCustomStatus(
  110. tr::lng_group_invite_joined_status(
  111. tr::now,
  112. lt_date,
  113. langDayOfMonthFull(base::unixtime::parse(date).date())));
  114. }
  115. QSize SubscriptionRow::rightActionSize() const {
  116. return _rightLabel ? _rightLabel->size : QSize();
  117. }
  118. QMargins SubscriptionRow::rightActionMargins() const {
  119. return QMargins(0, 0, st::boxRowPadding.right(), 0);
  120. }
  121. void SubscriptionRow::rightActionPaint(
  122. Painter &p,
  123. int x,
  124. int y,
  125. int outerWidth,
  126. bool selected,
  127. bool actionSelected) {
  128. if (_rightLabel) {
  129. return _rightLabel->draw(p, x, y, st::peerListBoxItem.height);
  130. }
  131. }
  132. class RequestedRow final : public PeerListRow {
  133. public:
  134. RequestedRow(not_null<PeerData*> peer, TimeId date);
  135. QSize rightActionSize() const override;
  136. QMargins rightActionMargins() const override;
  137. void rightActionPaint(
  138. Painter &p,
  139. int x,
  140. int y,
  141. int outerWidth,
  142. bool selected,
  143. bool actionSelected) override;
  144. };
  145. RequestedRow::RequestedRow(not_null<PeerData*> peer, TimeId date)
  146. : PeerListRow(peer) {
  147. setCustomStatus(PrepareRequestedRowStatus(date));
  148. }
  149. QSize RequestedRow::rightActionSize() const {
  150. return QSize(
  151. st::inviteLinkThreeDotsIcon.width(),
  152. st::inviteLinkThreeDotsIcon.height());
  153. }
  154. QMargins RequestedRow::rightActionMargins() const {
  155. return QMargins(
  156. 0,
  157. (st::peerListBoxItem.height - rightActionSize().height()) / 2,
  158. st::inviteLinkThreeDotsSkip,
  159. 0);
  160. }
  161. void RequestedRow::rightActionPaint(
  162. Painter &p,
  163. int x,
  164. int y,
  165. int outerWidth,
  166. bool selected,
  167. bool actionSelected) {
  168. (actionSelected
  169. ? st::inviteLinkThreeDotsIconOver
  170. : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);
  171. }
  172. class Controller final
  173. : public PeerListController
  174. , public base::has_weak_ptr {
  175. public:
  176. enum class Role {
  177. Requested,
  178. Joined,
  179. };
  180. Controller(
  181. not_null<PeerData*> peer,
  182. not_null<UserData*> admin,
  183. rpl::producer<LinkData> data,
  184. Role role);
  185. void prepare() override;
  186. void loadMoreRows() override;
  187. void rowClicked(not_null<PeerListRow*> row) override;
  188. void rowRightActionClicked(not_null<PeerListRow*> row) override;
  189. Main::Session &session() const override;
  190. rpl::producer<int> boxHeightValue() const override;
  191. int descriptionTopSkipMin() const override;
  192. struct Processed {
  193. not_null<UserData*> user;
  194. bool approved = false;
  195. };
  196. [[nodiscard]] rpl::producer<Processed> processed() const {
  197. return _processed.events();
  198. }
  199. private:
  200. base::unique_qptr<Ui::PopupMenu> rowContextMenu(
  201. QWidget *parent,
  202. not_null<PeerListRow*> row) override;
  203. void setupAboveJoinedWidget();
  204. void appendSlice(const Api::JoinedByLinkSlice &slice);
  205. void addHeaderBlock(not_null<Ui::VerticalLayout*> container);
  206. not_null<Ui::SlideWrap<>*> addRequestedListBlock(
  207. not_null<Ui::VerticalLayout*> container);
  208. void updateWithProcessed(Processed processed);
  209. [[nodiscard]] rpl::producer<LinkData> dataValue() const;
  210. [[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
  211. QWidget *parent,
  212. not_null<PeerListRow*> row);
  213. void processRequest(not_null<UserData*> user, bool approved);
  214. const not_null<PeerData*> _peer;
  215. const Role _role = Role::Joined;
  216. rpl::variable<LinkData> _data;
  217. base::unique_qptr<Ui::PopupMenu> _menu;
  218. rpl::event_stream<Processed> _processed;
  219. QString _link;
  220. bool _revoked = false;
  221. mtpRequestId _requestId = 0;
  222. std::optional<Api::JoinedByLinkUser> _lastUser;
  223. bool _allLoaded = false;
  224. Ui::RpWidget *_headerWidget = nullptr;
  225. rpl::variable<int> _addedHeight;
  226. MTP::Sender _api;
  227. rpl::lifetime _lifetime;
  228. };
  229. class SingleRowController final : public PeerListController {
  230. public:
  231. SingleRowController(
  232. not_null<Data::Thread*> thread,
  233. rpl::producer<QString> status,
  234. Fn<void()> clicked);
  235. void prepare() override;
  236. void loadMoreRows() override;
  237. void rowClicked(not_null<PeerListRow*> row) override;
  238. Main::Session &session() const override;
  239. private:
  240. const not_null<Main::Session*> _session;
  241. const base::weak_ptr<Data::Thread> _thread;
  242. rpl::producer<QString> _status;
  243. Fn<void()> _clicked;
  244. rpl::lifetime _lifetime;
  245. };
  246. [[nodiscard]] bool ClosingLinkBox(const LinkData &updated, bool revoked) {
  247. return updated.link.isEmpty() || (!revoked && updated.revoked);
  248. }
  249. #if 0
  250. QImage QrExact(const Qr::Data &data, int pixel, QColor color) {
  251. const auto image = [](int size) {
  252. auto result = QImage(
  253. size,
  254. size,
  255. QImage::Format_ARGB32_Premultiplied);
  256. result.fill(Qt::transparent);
  257. {
  258. QPainter p(&result);
  259. const auto skip = size / 12;
  260. const auto logoSize = size - 2 * skip;
  261. p.drawImage(
  262. skip,
  263. skip,
  264. Window::LogoNoMargin().scaled(
  265. logoSize,
  266. logoSize,
  267. Qt::IgnoreAspectRatio,
  268. Qt::SmoothTransformation));
  269. }
  270. return result;
  271. };
  272. return Qr::ReplaceCenter(
  273. Qr::Generate(data, pixel, color),
  274. image(Qr::ReplaceSize(data, pixel)));
  275. }
  276. QImage Qr(const Qr::Data &data, int pixel, int max = 0) {
  277. Expects(data.size > 0);
  278. if (max > 0 && data.size * pixel > max) {
  279. pixel = std::max(max / data.size, 1);
  280. }
  281. return QrExact(data, pixel * style::DevicePixelRatio(), st::windowFg->c);
  282. }
  283. QImage Qr(const QString &text, int pixel, int max) {
  284. return Qr(Qr::Encode(text), pixel, max);
  285. }
  286. QImage QrForShare(const QString &text) {
  287. const auto data = Qr::Encode(text);
  288. const auto size = (kShareQrSize - 2 * kShareQrPadding);
  289. const auto image = QrExact(data, size / data.size, Qt::black);
  290. auto result = QImage(
  291. kShareQrPadding * 2 + image.width(),
  292. kShareQrPadding * 2 + image.height(),
  293. QImage::Format_ARGB32_Premultiplied);
  294. result.fill(Qt::white);
  295. {
  296. auto p = QPainter(&result);
  297. p.drawImage(kShareQrPadding, kShareQrPadding, image);
  298. }
  299. return result;
  300. }
  301. void QrBox(
  302. not_null<Ui::GenericBox*> box,
  303. const QString &link,
  304. rpl::producer<QString> title,
  305. rpl::producer<QString> about,
  306. Fn<void(QImage, std::shared_ptr<Ui::Show>)> share) {
  307. box->setTitle(std::move(title));
  308. box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
  309. const auto copyCallback = [=, show = box->uiShow()] {
  310. share(QrForShare(link), show);
  311. };
  312. const auto qr = Qr(
  313. link,
  314. st::inviteLinkQrPixel,
  315. st::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right());
  316. const auto size = qr.width() / style::DevicePixelRatio();
  317. const auto height = st::inviteLinkQrSkip * 2 + size;
  318. const auto container = box->addRow(
  319. object_ptr<Ui::BoxContentDivider>(box, height),
  320. st::inviteLinkQrMargin);
  321. const auto button = Ui::CreateChild<Ui::AbstractButton>(container);
  322. button->resize(size, size);
  323. button->paintRequest(
  324. ) | rpl::start_with_next([=] {
  325. QPainter(button).drawImage(QRect(0, 0, size, size), qr);
  326. }, button->lifetime());
  327. container->widthValue(
  328. ) | rpl::start_with_next([=](int width) {
  329. button->move((width - size) / 2, st::inviteLinkQrSkip);
  330. }, button->lifetime());
  331. button->setClickedCallback(copyCallback);
  332. box->addRow(
  333. object_ptr<Ui::FlatLabel>(
  334. box,
  335. std::move(about),
  336. st::boxLabel),
  337. st::inviteLinkQrValuePadding);
  338. box->addLeftButton(tr::lng_group_invite_context_copy(), copyCallback);
  339. }
  340. #endif
  341. Controller::Controller(
  342. not_null<PeerData*> peer,
  343. not_null<UserData*> admin,
  344. rpl::producer<LinkData> data,
  345. Role role)
  346. : _peer(peer)
  347. , _role(role)
  348. , _data(LinkData{ .admin = admin })
  349. , _api(&session().api().instance()) {
  350. _data = std::move(data);
  351. const auto current = _data.current();
  352. _link = current.link;
  353. _revoked = current.revoked;
  354. }
  355. rpl::producer<LinkData> Controller::dataValue() const {
  356. return _data.value(
  357. ) | rpl::filter([=](const LinkData &data) {
  358. return !ClosingLinkBox(data, _revoked);
  359. });
  360. }
  361. void Controller::addHeaderBlock(not_null<Ui::VerticalLayout*> container) {
  362. using namespace Settings;
  363. const auto current = _data.current();
  364. const auto revoked = current.revoked;
  365. const auto link = current.link;
  366. const auto admin = current.admin;
  367. const auto weak = Ui::MakeWeak(container);
  368. const auto copyLink = crl::guard(weak, [=] {
  369. CopyInviteLink(delegate()->peerListUiShow(), link);
  370. });
  371. const auto shareLink = crl::guard(weak, [=, peer = _peer] {
  372. delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(peer, link));
  373. });
  374. const auto getLinkQr = crl::guard(weak, [=] {
  375. delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
  376. _peer,
  377. link,
  378. tr::lng_group_invite_qr_title(),
  379. tr::lng_group_invite_qr_about()));
  380. });
  381. const auto revokeLink = crl::guard(weak, [=] {
  382. delegate()->peerListUiShow()->showBox(
  383. RevokeLinkBox(_peer, admin, link));
  384. });
  385. const auto editLink = crl::guard(weak, [=] {
  386. delegate()->peerListUiShow()->showBox(
  387. EditLinkBox(_peer, _data.current()));
  388. });
  389. const auto deleteLink = crl::guard(weak, [=] {
  390. delegate()->peerListUiShow()->showBox(
  391. DeleteLinkBox(_peer, admin, link));
  392. });
  393. const auto createMenu = [=] {
  394. auto result = base::make_unique_q<Ui::PopupMenu>(
  395. container,
  396. st::popupMenuWithIcons);
  397. if (revoked) {
  398. result->addAction(
  399. tr::lng_group_invite_context_delete(tr::now),
  400. deleteLink,
  401. &st::menuIconDelete);
  402. } else {
  403. result->addAction(
  404. tr::lng_group_invite_context_copy(tr::now),
  405. copyLink,
  406. &st::menuIconCopy);
  407. result->addAction(
  408. tr::lng_group_invite_context_share(tr::now),
  409. shareLink,
  410. &st::menuIconShare);
  411. result->addAction(
  412. tr::lng_group_invite_context_qr(tr::now),
  413. getLinkQr,
  414. &st::menuIconQrCode);
  415. if (!admin->isBot()) {
  416. result->addAction(
  417. tr::lng_group_invite_context_edit(tr::now),
  418. editLink,
  419. &st::menuIconEdit);
  420. result->addAction(
  421. tr::lng_group_invite_context_revoke(tr::now),
  422. revokeLink,
  423. &st::menuIconRemove);
  424. }
  425. }
  426. return result;
  427. };
  428. const auto prefix = u"https://"_q;
  429. const auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(
  430. container,
  431. rpl::single(link.startsWith(prefix)
  432. ? link.mid(prefix.size())
  433. : link),
  434. createMenu);
  435. container->add(
  436. label->take(),
  437. st::inviteLinkFieldPadding);
  438. label->clicks(
  439. ) | rpl::start_with_next(copyLink, label->lifetime());
  440. const auto reactivateWrap = container->add(
  441. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  442. container,
  443. object_ptr<Ui::VerticalLayout>(
  444. container)));
  445. const auto copyShareWrap = container->add(
  446. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  447. container,
  448. object_ptr<Ui::VerticalLayout>(
  449. container)));
  450. AddReactivateLinkButton(reactivateWrap->entity(), editLink);
  451. AddCopyShareLinkButtons(copyShareWrap->entity(), copyLink, shareLink);
  452. if (revoked) {
  453. AddDeleteLinkButton(container, deleteLink);
  454. }
  455. Ui::AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2);
  456. auto grayLabelText = dataValue(
  457. ) | rpl::map([=](const LinkData &data) {
  458. const auto usageExpired = (data.usageLimit > 0)
  459. && (data.usageLimit <= data.usage);
  460. return usageExpired
  461. ? tr::lng_group_invite_used_about()
  462. : tr::lng_group_invite_expires_at(
  463. lt_when,
  464. rpl::single(langDateTime(
  465. base::unixtime::parse(data.expireDate))));
  466. }) | rpl::flatten_latest();
  467. const auto redLabelWrap = container->add(
  468. object_ptr<Ui::SlideWrap<Ui::DividerLabel>>(
  469. container,
  470. object_ptr<Ui::DividerLabel>(
  471. container,
  472. object_ptr<Ui::FlatLabel>(
  473. container,
  474. tr::lng_group_invite_expired_about(),
  475. st::boxAttentionDividerLabel),
  476. st::defaultBoxDividerLabelPadding)));
  477. const auto grayLabelWrap = container->add(
  478. object_ptr<Ui::SlideWrap<Ui::DividerLabel>>(
  479. container,
  480. object_ptr<Ui::DividerLabel>(
  481. container,
  482. object_ptr<Ui::FlatLabel>(
  483. container,
  484. std::move(grayLabelText),
  485. st::boxDividerLabel),
  486. st::defaultBoxDividerLabelPadding)));
  487. const auto justDividerWrap = container->add(
  488. object_ptr<Ui::SlideWrap<>>(
  489. container,
  490. object_ptr<Ui::BoxContentDivider>(container)));
  491. Ui::AddSkip(container);
  492. dataValue(
  493. ) | rpl::start_with_next([=](const LinkData &data) {
  494. const auto now = base::unixtime::now();
  495. const auto expired = IsExpiredLink(data, now);
  496. reactivateWrap->toggle(
  497. !revoked && expired && !admin->isBot(),
  498. anim::type::instant);
  499. copyShareWrap->toggle(!revoked && !expired, anim::type::instant);
  500. const auto timeExpired = (data.expireDate > 0)
  501. && (data.expireDate <= now);
  502. const auto usageExpired = (data.usageLimit > 0)
  503. && (data.usageLimit <= data.usage);
  504. redLabelWrap->toggle(!revoked && timeExpired, anim::type::instant);
  505. grayLabelWrap->toggle(
  506. !revoked && !timeExpired && (data.expireDate > 0 || usageExpired),
  507. anim::type::instant);
  508. justDividerWrap->toggle(
  509. revoked || (!data.expireDate && !expired),
  510. anim::type::instant);
  511. }, lifetime());
  512. }
  513. not_null<Ui::SlideWrap<>*> Controller::addRequestedListBlock(
  514. not_null<Ui::VerticalLayout*> container) {
  515. using namespace Settings;
  516. auto result = container->add(
  517. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  518. container,
  519. object_ptr<Ui::VerticalLayout>(
  520. container)));
  521. const auto wrap = result->entity();
  522. // Make this container occupy full width.
  523. wrap->add(object_ptr<Ui::RpWidget>(wrap));
  524. Ui::AddDivider(wrap);
  525. Ui::AddSkip(wrap);
  526. auto requestedCount = dataValue(
  527. ) | rpl::filter([](const LinkData &data) {
  528. return data.requested > 0;
  529. }) | rpl::map([=](const LinkData &data) {
  530. return float64(data.requested);
  531. });
  532. Ui::AddSubsectionTitle(
  533. wrap,
  534. tr::lng_group_invite_requested_full(
  535. lt_count_decimal,
  536. std::move(requestedCount)));
  537. class Delegate final : public PeerListContentDelegateSimple {
  538. public:
  539. explicit Delegate(std::shared_ptr<Main::SessionShow> show)
  540. : _show(std::move(show)) {
  541. }
  542. std::shared_ptr<Main::SessionShow> peerListUiShow() override {
  543. return _show;
  544. }
  545. private:
  546. const std::shared_ptr<Main::SessionShow> _show;
  547. };
  548. const auto delegate = container->lifetime().make_state<Delegate>(
  549. this->delegate()->peerListUiShow());
  550. const auto controller = container->lifetime().make_state<
  551. Controller
  552. >(_peer, _data.current().admin, _data.value(), Role::Requested);
  553. const auto content = container->add(object_ptr<PeerListContent>(
  554. container,
  555. controller));
  556. delegate->setContent(content);
  557. controller->setDelegate(delegate);
  558. controller->processed(
  559. ) | rpl::start_with_next([=](Processed processed) {
  560. updateWithProcessed(processed);
  561. }, lifetime());
  562. return result;
  563. }
  564. void Controller::prepare() {
  565. if (_role == Role::Joined) {
  566. setupAboveJoinedWidget();
  567. _allLoaded = (_data.current().usage == 0);
  568. const auto &inviteLinks = session().api().inviteLinks();
  569. const auto slice = inviteLinks.joinedFirstSliceLoaded(_peer, _link);
  570. if (slice) {
  571. appendSlice(*slice);
  572. }
  573. } else {
  574. _allLoaded = (_data.current().requested == 0);
  575. }
  576. loadMoreRows();
  577. }
  578. void Controller::updateWithProcessed(Processed processed) {
  579. const auto user = processed.user;
  580. auto updated = _data.current();
  581. if (processed.approved) {
  582. ++updated.usage;
  583. if (!delegate()->peerListFindRow(user->id.value)) {
  584. delegate()->peerListPrependRow(
  585. std::make_unique<PeerListRow>(user));
  586. delegate()->peerListRefreshRows();
  587. }
  588. }
  589. if (updated.requested > 0) {
  590. --updated.requested;
  591. }
  592. session().api().inviteLinks().applyExternalUpdate(_peer, updated);
  593. }
  594. void Controller::setupAboveJoinedWidget() {
  595. using namespace Settings;
  596. auto header = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
  597. const auto container = header.data();
  598. const auto current = _data.current();
  599. const auto revoked = current.revoked;
  600. if (revoked || !current.permanent) {
  601. addHeaderBlock(container);
  602. }
  603. if (current.subscription) {
  604. const auto &st = st::peerListSingleRow.item;
  605. Ui::AddSubsectionTitle(
  606. container,
  607. tr::lng_group_invite_subscription_info_subtitle());
  608. const auto widget = container->add(
  609. CreateSkipWidget(container, st.height));
  610. const auto name = widget->lifetime().make_state<Ui::Text::String>();
  611. auto userpic = QImage(
  612. Size(st.photoSize) * style::DevicePixelRatio(),
  613. QImage::Format_ARGB32_Premultiplied);
  614. {
  615. constexpr auto kGreenIndex = 3;
  616. const auto colors = Ui::EmptyUserpic::UserpicColor(kGreenIndex);
  617. auto emptyUserpic = Ui::EmptyUserpic(colors, {});
  618. userpic.setDevicePixelRatio(style::DevicePixelRatio());
  619. userpic.fill(Qt::transparent);
  620. auto p = QPainter(&userpic);
  621. emptyUserpic.paintCircle(p, 0, 0, st.photoSize, st.photoSize);
  622. auto svg = QSvgRenderer(u":/gui/links_subscription.svg"_q);
  623. const auto size = st.photoSize / 4. * 3.;
  624. const auto r = QRectF(
  625. (st.photoSize - size) / 2.,
  626. (st.photoSize - size) / 2.,
  627. size,
  628. size);
  629. p.setPen(st::historyPeerUserpicFg);
  630. p.setBrush(Qt::NoBrush);
  631. svg.render(&p, r);
  632. }
  633. name->setMarkedText(
  634. st.nameStyle,
  635. current.usage
  636. ? tr::lng_group_invite_subscription_info_title(
  637. tr::now,
  638. lt_emoji,
  639. session().data().customEmojiManager().creditsEmoji(),
  640. lt_price,
  641. { QString::number(current.subscription.credits) },
  642. lt_multiplier,
  643. TextWithEntities{ .text = QString(QChar(0x00D7)) },
  644. lt_total,
  645. { QString::number(current.usage) },
  646. Ui::Text::WithEntities)
  647. : tr::lng_group_invite_subscription_info_title_none(
  648. tr::now,
  649. lt_emoji,
  650. session().data().customEmojiManager().creditsEmoji(),
  651. lt_price,
  652. { QString::number(current.subscription.credits) },
  653. Ui::Text::WithEntities),
  654. kMarkupTextOptions,
  655. Core::TextContext({
  656. .session = &session(),
  657. .repaint = [=] { widget->update(); },
  658. }));
  659. auto &lifetime = widget->lifetime();
  660. const auto rateValue = lifetime.make_state<rpl::variable<float64>>(
  661. session().credits().rateValue(_peer));
  662. const auto currency = u"USD"_q;
  663. const auto allCredits = current.subscription.credits * current.usage;
  664. widget->paintRequest(
  665. ) | rpl::start_with_next([=] {
  666. auto p = Painter(widget);
  667. p.setBrush(Qt::NoBrush);
  668. p.setPen(st.nameFg);
  669. name->draw(p, {
  670. .position = st.namePosition,
  671. .outerWidth = widget->width() - name->maxWidth(),
  672. .availableWidth = widget->width() - name->maxWidth(),
  673. });
  674. p.drawImage(st.photoPosition, userpic);
  675. const auto rate = rateValue->current();
  676. const auto status = (allCredits <= 0)
  677. ? tr::lng_group_invite_no_joined(tr::now)
  678. : (rate > 0)
  679. ? tr::lng_group_invite_subscription_info_about(
  680. tr::now,
  681. lt_total,
  682. Ui::FillAmountAndCurrency(allCredits * rate, currency))
  683. : QString();
  684. p.setPen(st.statusFg);
  685. p.setFont(st::contactsStatusFont);
  686. p.drawTextLeft(
  687. st.statusPosition.x(),
  688. st.statusPosition.y(),
  689. widget->width() - st.statusPosition.x(),
  690. status);
  691. }, widget->lifetime());
  692. }
  693. Ui::AddSubsectionTitle(
  694. container,
  695. tr::lng_group_invite_created_by());
  696. AddSinglePeerRow(
  697. container,
  698. current.admin,
  699. rpl::single(langDateTime(base::unixtime::parse(current.date))));
  700. Ui::AddSkip(container, st::membersMarginBottom);
  701. auto requestedWrap = addRequestedListBlock(container);
  702. const auto listHeaderWrap = container->add(
  703. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  704. container,
  705. object_ptr<Ui::VerticalLayout>(
  706. container)));
  707. const auto listHeader = listHeaderWrap->entity();
  708. // Make this container occupy full width.
  709. listHeader->add(object_ptr<Ui::RpWidget>(listHeader));
  710. Ui::AddDivider(listHeader);
  711. Ui::AddSkip(listHeader);
  712. auto listHeaderText = dataValue(
  713. ) | rpl::map([=](const LinkData &data) {
  714. const auto now = base::unixtime::now();
  715. const auto timeExpired = (data.expireDate > 0)
  716. && (data.expireDate <= now);
  717. if (!revoked && !data.usage && data.usageLimit > 0 && !timeExpired) {
  718. auto description = object_ptr<Ui::FlatLabel>(
  719. nullptr,
  720. tr::lng_group_invite_can_join_via_link(
  721. tr::now,
  722. lt_count,
  723. data.usageLimit),
  724. computeListSt().about);
  725. if (!delegate()->peerListFullRowsCount()) {
  726. using namespace rpl::mappers;
  727. _addedHeight = description->heightValue(
  728. ) | rpl::map(_1
  729. + st::membersAboutLimitPadding.top()
  730. + st::membersAboutLimitPadding.bottom());
  731. }
  732. delegate()->peerListSetDescription(std::move(description));
  733. } else {
  734. _addedHeight = std::max(
  735. data.usage,
  736. delegate()->peerListFullRowsCount()
  737. ) * computeListSt().item.height;
  738. delegate()->peerListSetDescription(nullptr);
  739. }
  740. listHeaderWrap->toggle(
  741. !revoked && (data.usage || (data.usageLimit > 0 && !timeExpired)),
  742. anim::type::instant);
  743. delegate()->peerListRefreshRows();
  744. return data.usage
  745. ? tr::lng_group_invite_joined(
  746. lt_count,
  747. rpl::single(float64(data.usage)))
  748. : tr::lng_group_invite_no_joined();
  749. }) | rpl::flatten_latest();
  750. const auto listTitle = AddSubsectionTitle(
  751. listHeader,
  752. std::move(listHeaderText));
  753. auto remainingText = dataValue(
  754. ) | rpl::map([=](const LinkData &data) {
  755. return !data.usageLimit
  756. ? QString()
  757. : tr::lng_group_invite_remaining(
  758. tr::now,
  759. lt_count_decimal,
  760. std::max(data.usageLimit - data.usage, 0));
  761. });
  762. const auto remaining = Ui::CreateChild<Ui::FlatLabel>(
  763. listHeader,
  764. std::move(remainingText),
  765. st::inviteLinkTitleRight);
  766. dataValue(
  767. ) | rpl::start_with_next([=](const LinkData &data) {
  768. remaining->setTextColorOverride(
  769. (data.usageLimit && (data.usageLimit <= data.usage)
  770. ? std::make_optional(st::boxTextFgError->c)
  771. : std::nullopt));
  772. if (revoked || (!data.usage && data.usageLimit > 0)) {
  773. remaining->hide();
  774. } else {
  775. remaining->show();
  776. }
  777. requestedWrap->toggle(data.requested > 0, anim::type::instant);
  778. }, remaining->lifetime());
  779. rpl::combine(
  780. listTitle->positionValue(),
  781. remaining->widthValue(),
  782. listHeader->widthValue()
  783. ) | rpl::start_with_next([=](
  784. QPoint position,
  785. int width,
  786. int outerWidth) {
  787. remaining->moveToRight(position.x(), position.y(), outerWidth);
  788. }, remaining->lifetime());
  789. _headerWidget = header.data();
  790. delegate()->peerListSetAboveWidget(std::move(header));
  791. }
  792. void Controller::loadMoreRows() {
  793. if (_requestId || _allLoaded) {
  794. return;
  795. }
  796. using Flag = MTPmessages_GetChatInviteImporters::Flag;
  797. _requestId = _api.request(MTPmessages_GetChatInviteImporters(
  798. MTP_flags(Flag::f_link
  799. | (_role == Role::Requested ? Flag::f_requested : Flag(0))),
  800. _peer->input,
  801. MTP_string(_link),
  802. MTPstring(), // q
  803. MTP_int(_lastUser ? _lastUser->date : 0),
  804. _lastUser ? _lastUser->user->inputUser : MTP_inputUserEmpty(),
  805. MTP_int(_lastUser ? kPerPage : kFirstPage)
  806. )).done([=](const MTPmessages_ChatInviteImporters &result) {
  807. _requestId = 0;
  808. auto slice = Api::ParseJoinedByLinkSlice(_peer, result);
  809. _allLoaded = slice.users.empty();
  810. appendSlice(slice);
  811. }).fail([=] {
  812. _requestId = 0;
  813. _allLoaded = true;
  814. }).send();
  815. }
  816. void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) {
  817. for (const auto &user : slice.users) {
  818. _lastUser = user;
  819. auto row = (_role == Role::Requested)
  820. ? std::make_unique<RequestedRow>(user.user, user.date)
  821. : (_data.current().subscription)
  822. ? std::make_unique<SubscriptionRow>(
  823. user.user,
  824. user.date,
  825. _data.current().subscription)
  826. : std::make_unique<PeerListRow>(user.user);
  827. if (_role != Role::Requested && user.viaFilterLink) {
  828. row->setCustomStatus(
  829. tr::lng_group_invite_joined_via_filter(tr::now));
  830. }
  831. delegate()->peerListAppendRow(std::move(row));
  832. }
  833. delegate()->peerListRefreshRows();
  834. if (delegate()->peerListFullRowsCount() > 0) {
  835. _addedHeight = std::max(
  836. _data.current().usage,
  837. delegate()->peerListFullRowsCount()
  838. ) * computeListSt().item.height;
  839. }
  840. }
  841. void Controller::rowClicked(not_null<PeerListRow*> row) {
  842. if (!_data.current().subscription) {
  843. return ShowPeerInfoSync(row->peer());
  844. }
  845. const auto channel = _peer;
  846. const auto data = _data.current();
  847. const auto show = delegate()->peerListUiShow();
  848. show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
  849. const auto w = Core::App().findWindow(box);
  850. const auto controller = w ? w->sessionController() : nullptr;
  851. if (!controller) {
  852. return;
  853. }
  854. box->setStyle(st::giveawayGiftCodeBox);
  855. box->setNoContentMargin(true);
  856. const auto content = box->verticalLayout();
  857. Ui::AddSkip(content);
  858. Ui::AddSkip(content);
  859. Ui::AddSkip(content);
  860. const auto photoSize = st::boostReplaceUserpic.photoSize;
  861. const auto session = &row->peer()->session();
  862. content->add(object_ptr<Ui::CenterWrap<>>(
  863. content,
  864. Settings::SubscriptionUserpic(content, channel, photoSize)));
  865. Ui::AddSkip(content);
  866. Ui::AddSkip(content);
  867. box->addRow(object_ptr<Ui::CenterWrap<>>(
  868. box,
  869. object_ptr<Ui::FlatLabel>(
  870. box,
  871. tr::lng_credits_box_subscription_title(),
  872. st::creditsBoxAboutTitle)));
  873. Ui::AddSkip(content);
  874. const auto subtitle1 = box->addRow(
  875. object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
  876. box,
  877. object_ptr<Ui::FlatLabel>(
  878. box,
  879. st::creditsTopupPrice)))->entity();
  880. subtitle1->setMarkedText(
  881. tr::lng_credits_subscription_subtitle(
  882. tr::now,
  883. lt_emoji,
  884. session->data().customEmojiManager().creditsEmoji(),
  885. lt_cost,
  886. { QString::number(data.subscription.credits) },
  887. Ui::Text::WithEntities),
  888. Core::TextContext({ .session = session }));
  889. const auto subtitle2 = box->addRow(
  890. object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
  891. box,
  892. object_ptr<Ui::FlatLabel>(
  893. box,
  894. st::creditsTopupPrice)))->entity();
  895. session->credits().rateValue(
  896. channel
  897. ) | rpl::start_with_next([=, currency = u"USD"_q](float64 rate) {
  898. subtitle2->setText(
  899. tr::lng_credits_subscriber_subtitle(
  900. tr::now,
  901. lt_total,
  902. Ui::FillAmountAndCurrency(
  903. data.subscription.credits * rate,
  904. currency)));
  905. }, subtitle2->lifetime());
  906. Ui::AddSkip(content);
  907. Ui::AddSkip(content);
  908. const auto show = controller->uiShow();
  909. AddSubscriberEntryTable(show, content, {}, row->peer(), data.date);
  910. Ui::AddSkip(content);
  911. Ui::AddSkip(content);
  912. box->addRow(object_ptr<Ui::CenterWrap<>>(
  913. box,
  914. object_ptr<Ui::FlatLabel>(
  915. box,
  916. tr::lng_credits_box_out_about(
  917. lt_link,
  918. tr::lng_payments_terms_link(
  919. ) | Ui::Text::ToLink(
  920. tr::lng_credits_box_out_about_link(tr::now)),
  921. Ui::Text::WithEntities),
  922. st::creditsBoxAboutDivider)));
  923. const auto button = box->addButton(tr::lng_box_ok(), [=] {
  924. box->closeBox();
  925. });
  926. const auto buttonWidth = st::boxWidth
  927. - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
  928. button->widthValue() | rpl::filter([=] {
  929. return (button->widthNoMargins() != buttonWidth);
  930. }) | rpl::start_with_next([=] {
  931. button->resizeToWidth(buttonWidth);
  932. }, button->lifetime());
  933. }));
  934. }
  935. void Controller::rowRightActionClicked(not_null<PeerListRow*> row) {
  936. if (_role != Role::Requested || _data.current().subscription) {
  937. return;
  938. }
  939. delegate()->peerListShowRowMenu(row, true);
  940. }
  941. base::unique_qptr<Ui::PopupMenu> Controller::rowContextMenu(
  942. QWidget *parent,
  943. not_null<PeerListRow*> row) {
  944. auto result = createRowContextMenu(parent, row);
  945. if (result) {
  946. // First clear _menu value, so that we don't check row positions yet.
  947. base::take(_menu);
  948. // Here unique_qptr is used like a shared pointer, where
  949. // not the last destroyed pointer destroys the object, but the first.
  950. _menu = base::unique_qptr<Ui::PopupMenu>(result.get());
  951. }
  952. return result;
  953. }
  954. base::unique_qptr<Ui::PopupMenu> Controller::createRowContextMenu(
  955. QWidget *parent,
  956. not_null<PeerListRow*> row) {
  957. const auto user = row->peer()->asUser();
  958. Assert(user != nullptr);
  959. auto result = base::make_unique_q<Ui::PopupMenu>(
  960. parent,
  961. st::popupMenuWithIcons);
  962. const auto add = _peer->isBroadcast()
  963. ? tr::lng_group_requests_add_channel(tr::now)
  964. : tr::lng_group_requests_add(tr::now);
  965. result->addAction(add, [=] {
  966. processRequest(user, true);
  967. }, &st::menuIconInvite);
  968. result->addAction(tr::lng_group_requests_dismiss(tr::now), [=] {
  969. processRequest(user, false);
  970. }, &st::menuIconRemove);
  971. return result;
  972. }
  973. void Controller::processRequest(
  974. not_null<UserData*> user,
  975. bool approved) {
  976. const auto done = crl::guard(this, [=] {
  977. _processed.fire({ user, approved });
  978. if (const auto row = delegate()->peerListFindRow(user->id.value)) {
  979. delegate()->peerListRemoveRow(row);
  980. delegate()->peerListRefreshRows();
  981. }
  982. if (approved) {
  983. delegate()->peerListUiShow()->showToast((_peer->isBroadcast()
  984. ? tr::lng_group_requests_was_added_channel
  985. : tr::lng_group_requests_was_added)(
  986. tr::now,
  987. lt_user,
  988. Ui::Text::Bold(user->name()),
  989. Ui::Text::WithEntities));
  990. }
  991. });
  992. const auto fail = crl::guard(this, [=] {
  993. _processed.fire({ user, false });
  994. });
  995. session().api().inviteLinks().processRequest(
  996. _peer,
  997. _data.current().link,
  998. user,
  999. approved,
  1000. done,
  1001. fail);
  1002. }
  1003. Main::Session &Controller::session() const {
  1004. return _peer->session();
  1005. }
  1006. rpl::producer<int> Controller::boxHeightValue() const {
  1007. Expects(_headerWidget != nullptr);
  1008. return rpl::combine(
  1009. _headerWidget->heightValue(),
  1010. _addedHeight.value()
  1011. ) | rpl::map([=](int header, int description) {
  1012. const auto wrapped = description
  1013. ? (computeListSt().padding.top()
  1014. + description
  1015. + computeListSt().padding.bottom())
  1016. : 0;
  1017. return std::min(header + wrapped, st::boxMaxListHeight);
  1018. });
  1019. }
  1020. int Controller::descriptionTopSkipMin() const {
  1021. return 0;
  1022. }
  1023. SingleRowController::SingleRowController(
  1024. not_null<Data::Thread*> thread,
  1025. rpl::producer<QString> status,
  1026. Fn<void()> clicked)
  1027. : _session(&thread->session())
  1028. , _thread(thread)
  1029. , _status(std::move(status))
  1030. , _clicked(std::move(clicked)) {
  1031. }
  1032. void SingleRowController::prepare() {
  1033. const auto strong = _thread.get();
  1034. if (!strong) {
  1035. return;
  1036. }
  1037. const auto topic = strong->asTopic();
  1038. auto row = topic
  1039. ? ChooseTopicBoxController::MakeRow(topic)
  1040. : std::make_unique<PeerListRow>(strong->peer());
  1041. const auto raw = row.get();
  1042. if (_status) {
  1043. std::move(
  1044. _status
  1045. ) | rpl::start_with_next([=](const QString &status) {
  1046. raw->setCustomStatus(status);
  1047. delegate()->peerListUpdateRow(raw);
  1048. }, _lifetime);
  1049. }
  1050. delegate()->peerListAppendRow(std::move(row));
  1051. delegate()->peerListRefreshRows();
  1052. if (topic) {
  1053. topic->destroyed() | rpl::start_with_next([=] {
  1054. while (delegate()->peerListFullRowsCount()) {
  1055. delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
  1056. }
  1057. delegate()->peerListRefreshRows();
  1058. }, _lifetime);
  1059. }
  1060. }
  1061. void SingleRowController::loadMoreRows() {
  1062. }
  1063. void SingleRowController::rowClicked(not_null<PeerListRow*> row) {
  1064. if (const auto onstack = _clicked) {
  1065. onstack();
  1066. } else {
  1067. ShowPeerInfoSync(row->peer());
  1068. }
  1069. }
  1070. Main::Session &SingleRowController::session() const {
  1071. return *_session;
  1072. }
  1073. } // namespace
  1074. bool IsExpiredLink(const Api::InviteLink &data, TimeId now) {
  1075. return (data.expireDate > 0 && data.expireDate <= now)
  1076. || (data.usageLimit > 0 && data.usageLimit <= data.usage);
  1077. }
  1078. void AddSinglePeerRow(
  1079. not_null<Ui::VerticalLayout*> container,
  1080. not_null<PeerData*> peer,
  1081. rpl::producer<QString> status,
  1082. Fn<void()> clicked) {
  1083. AddSinglePeerRow(
  1084. container,
  1085. peer->owner().history(peer),
  1086. std::move(status),
  1087. std::move(clicked));
  1088. }
  1089. void AddSinglePeerRow(
  1090. not_null<Ui::VerticalLayout*> container,
  1091. not_null<Data::Thread*> thread,
  1092. rpl::producer<QString> status,
  1093. Fn<void()> clicked) {
  1094. const auto delegate = container->lifetime().make_state<
  1095. PeerListContentDelegateSimple
  1096. >();
  1097. const auto controller = container->lifetime().make_state<
  1098. SingleRowController
  1099. >(thread, std::move(status), std::move(clicked));
  1100. controller->setStyleOverrides(thread->asTopic()
  1101. ? &st::chooseTopicList
  1102. : &st::peerListSingleRow);
  1103. const auto content = container->add(object_ptr<PeerListContent>(
  1104. container,
  1105. controller));
  1106. delegate->setContent(content);
  1107. controller->setDelegate(delegate);
  1108. }
  1109. void AddPermanentLinkBlock(
  1110. std::shared_ptr<Ui::Show> show,
  1111. not_null<Ui::VerticalLayout*> container,
  1112. not_null<PeerData*> peer,
  1113. not_null<UserData*> admin,
  1114. rpl::producer<Api::InviteLink> fromList) {
  1115. struct LinkData {
  1116. QString link;
  1117. int usage = 0;
  1118. };
  1119. const auto value = container->lifetime().make_state<
  1120. rpl::variable<LinkData>
  1121. >();
  1122. const auto currentLinkFields = container->lifetime().make_state<
  1123. Api::InviteLink
  1124. >(Api::InviteLink{ .admin = admin });
  1125. if (admin->isSelf()) {
  1126. *value = peer->session().changes().peerFlagsValue(
  1127. peer,
  1128. Data::PeerUpdate::Flag::InviteLinks
  1129. ) | rpl::map([=] {
  1130. const auto &links = peer->session().api().inviteLinks().myLinks(
  1131. peer).links;
  1132. const auto link = links.empty() ? nullptr : &links.front();
  1133. if (link && link->permanent && !link->revoked) {
  1134. *currentLinkFields = *link;
  1135. return LinkData{ link->link, link->usage };
  1136. }
  1137. return LinkData();
  1138. });
  1139. } else {
  1140. rpl::duplicate(
  1141. fromList
  1142. ) | rpl::start_with_next([=](const Api::InviteLink &link) {
  1143. *currentLinkFields = link;
  1144. }, container->lifetime());
  1145. *value = std::move(
  1146. fromList
  1147. ) | rpl::map([](const Api::InviteLink &link) {
  1148. return LinkData{ link.link, link.usage };
  1149. });
  1150. }
  1151. const auto weak = Ui::MakeWeak(container);
  1152. const auto copyLink = crl::guard(weak, [=] {
  1153. if (const auto current = value->current(); !current.link.isEmpty()) {
  1154. CopyInviteLink(show, current.link);
  1155. }
  1156. });
  1157. const auto shareLink = crl::guard(weak, [=] {
  1158. if (const auto current = value->current(); !current.link.isEmpty()) {
  1159. show->showBox(ShareInviteLinkBox(peer, current.link));
  1160. }
  1161. });
  1162. const auto getLinkQr = crl::guard(weak, [=] {
  1163. if (const auto current = value->current(); !current.link.isEmpty()) {
  1164. show->showBox(InviteLinkQrBox(
  1165. peer,
  1166. current.link,
  1167. tr::lng_group_invite_qr_title(),
  1168. tr::lng_group_invite_qr_about()));
  1169. }
  1170. });
  1171. const auto revokeLink = crl::guard(weak, [=] {
  1172. if (const auto current = value->current(); !current.link.isEmpty()) {
  1173. show->showBox(RevokeLinkBox(peer, admin, current.link, true));
  1174. }
  1175. });
  1176. auto link = value->value(
  1177. ) | rpl::map([=](const LinkData &data) {
  1178. const auto prefix = u"https://"_q;
  1179. return data.link.startsWith(prefix)
  1180. ? data.link.mid(prefix.size())
  1181. : data.link;
  1182. });
  1183. const auto createMenu = [=] {
  1184. auto result = base::make_unique_q<Ui::PopupMenu>(
  1185. container,
  1186. st::popupMenuWithIcons);
  1187. result->addAction(
  1188. tr::lng_group_invite_context_copy(tr::now),
  1189. copyLink,
  1190. &st::menuIconCopy);
  1191. result->addAction(
  1192. tr::lng_group_invite_context_share(tr::now),
  1193. shareLink,
  1194. &st::menuIconShare);
  1195. result->addAction(
  1196. tr::lng_group_invite_context_qr(tr::now),
  1197. getLinkQr,
  1198. &st::menuIconQrCode);
  1199. if (!admin->isBot()) {
  1200. result->addAction(
  1201. tr::lng_group_invite_context_revoke(tr::now),
  1202. revokeLink,
  1203. &st::menuIconRemove);
  1204. }
  1205. return result;
  1206. };
  1207. const auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(
  1208. container,
  1209. std::move(link),
  1210. createMenu);
  1211. container->add(
  1212. label->take(),
  1213. st::inviteLinkFieldPadding);
  1214. label->clicks(
  1215. ) | rpl::start_with_next(copyLink, label->lifetime());
  1216. AddCopyShareLinkButtons(container, copyLink, shareLink);
  1217. struct JoinedState {
  1218. QImage cachedUserpics;
  1219. std::vector<HistoryView::UserpicInRow> list;
  1220. int count = 0;
  1221. bool allUserpicsLoaded = false;
  1222. rpl::variable<Ui::JoinedCountContent> content;
  1223. rpl::lifetime lifetime;
  1224. };
  1225. const auto state = container->lifetime().make_state<JoinedState>();
  1226. const auto push = [=] {
  1227. HistoryView::GenerateUserpicsInRow(
  1228. state->cachedUserpics,
  1229. state->list,
  1230. st::inviteLinkUserpics,
  1231. 0);
  1232. state->allUserpicsLoaded = ranges::all_of(
  1233. state->list,
  1234. [](const HistoryView::UserpicInRow &element) {
  1235. return !element.peer->hasUserpic()
  1236. || !Ui::PeerUserpicLoading(element.view);
  1237. });
  1238. state->content = Ui::JoinedCountContent{
  1239. .count = state->count,
  1240. .userpics = state->cachedUserpics,
  1241. };
  1242. };
  1243. value->value(
  1244. ) | rpl::map([=](const LinkData &data) {
  1245. return peer->session().api().inviteLinks().joinedFirstSliceValue(
  1246. peer,
  1247. data.link,
  1248. data.usage);
  1249. }) | rpl::flatten_latest(
  1250. ) | rpl::start_with_next([=](const Api::JoinedByLinkSlice &slice) {
  1251. auto list = std::vector<HistoryView::UserpicInRow>();
  1252. list.reserve(slice.users.size());
  1253. for (const auto &item : slice.users) {
  1254. const auto i = ranges::find(
  1255. state->list,
  1256. item.user,
  1257. &HistoryView::UserpicInRow::peer);
  1258. if (i != end(state->list)) {
  1259. list.push_back(std::move(*i));
  1260. } else {
  1261. list.push_back({ item.user });
  1262. }
  1263. }
  1264. state->count = slice.count;
  1265. state->list = std::move(list);
  1266. push();
  1267. }, state->lifetime);
  1268. peer->session().downloaderTaskFinished(
  1269. ) | rpl::filter([=] {
  1270. return !state->allUserpicsLoaded;
  1271. }) | rpl::start_with_next([=] {
  1272. auto pushing = false;
  1273. state->allUserpicsLoaded = true;
  1274. for (const auto &element : state->list) {
  1275. if (!element.peer->hasUserpic()) {
  1276. continue;
  1277. } else if (element.peer->userpicUniqueKey(element.view)
  1278. != element.uniqueKey) {
  1279. pushing = true;
  1280. } else if (Ui::PeerUserpicLoading(element.view)) {
  1281. state->allUserpicsLoaded = false;
  1282. }
  1283. }
  1284. if (pushing) {
  1285. push();
  1286. }
  1287. }, state->lifetime);
  1288. Ui::AddJoinedCountButton(
  1289. container,
  1290. state->content.value(),
  1291. st::inviteLinkJoinedRowPadding
  1292. )->setClickedCallback([=] {
  1293. if (!currentLinkFields->link.isEmpty()) {
  1294. show->showBox(ShowInviteLinkBox(peer, *currentLinkFields));
  1295. }
  1296. });
  1297. container->add(object_ptr<Ui::SlideWrap<Ui::FixedHeightWidget>>(
  1298. container,
  1299. object_ptr<Ui::FixedHeightWidget>(
  1300. container,
  1301. st::inviteLinkJoinedRowPadding.bottom()))
  1302. )->setDuration(0)->toggleOn(state->content.value(
  1303. ) | rpl::map([=](const Ui::JoinedCountContent &content) {
  1304. return (content.count <= 0);
  1305. }));
  1306. }
  1307. void CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link) {
  1308. QGuiApplication::clipboard()->setText(link);
  1309. show->showToast(tr::lng_group_invite_copied(tr::now));
  1310. }
  1311. object_ptr<Ui::BoxContent> ShareInviteLinkBox(
  1312. not_null<PeerData*> peer,
  1313. const QString &link,
  1314. const QString &copied) {
  1315. return ShareInviteLinkBox(&peer->session(), link, copied);
  1316. }
  1317. object_ptr<Ui::BoxContent> ShareInviteLinkBox(
  1318. not_null<Main::Session*> session,
  1319. const QString &link,
  1320. const QString &copied) {
  1321. const auto sending = std::make_shared<bool>();
  1322. const auto box = std::make_shared<QPointer<ShareBox>>();
  1323. const auto showToast = [=](const QString &text) {
  1324. if (*box) {
  1325. (*box)->showToast(text);
  1326. }
  1327. };
  1328. auto copyCallback = [=] {
  1329. QGuiApplication::clipboard()->setText(link);
  1330. showToast(copied.isEmpty()
  1331. ? tr::lng_group_invite_copied(tr::now)
  1332. : copied);
  1333. };
  1334. auto countMessagesCallback = [=](const TextWithTags &comment) {
  1335. return 1;
  1336. };
  1337. auto submitCallback = [=](
  1338. std::vector<not_null<Data::Thread*>> &&result,
  1339. Fn<bool()> checkPaid,
  1340. TextWithTags &&comment,
  1341. Api::SendOptions options,
  1342. Data::ForwardOptions) {
  1343. if (*sending || result.empty()) {
  1344. return;
  1345. }
  1346. const auto errorWithThread = GetErrorForSending(
  1347. result,
  1348. { .text = &comment });
  1349. if (errorWithThread.error) {
  1350. if (*box) {
  1351. (*box)->uiShow()->showBox(MakeSendErrorBox(
  1352. errorWithThread,
  1353. result.size() > 1));
  1354. }
  1355. return;
  1356. } else if (!checkPaid()) {
  1357. return;
  1358. }
  1359. *sending = true;
  1360. if (!comment.text.isEmpty()) {
  1361. comment.text = link + "\n" + comment.text;
  1362. const auto add = link.size() + 1;
  1363. for (auto &tag : comment.tags) {
  1364. tag.offset += add;
  1365. }
  1366. } else {
  1367. comment.text = link;
  1368. }
  1369. auto &api = session->api();
  1370. for (const auto thread : result) {
  1371. auto message = Api::MessageToSend(
  1372. Api::SendAction(thread, options));
  1373. message.textWithTags = comment;
  1374. message.action.clearDraft = false;
  1375. api.sendMessage(std::move(message));
  1376. }
  1377. if (*box) {
  1378. showToast(tr::lng_share_done(tr::now));
  1379. (*box)->closeBox();
  1380. }
  1381. };
  1382. auto filterCallback = [](not_null<Data::Thread*> thread) {
  1383. if (const auto user = thread->peer()->asUser()) {
  1384. if (user->canSendIgnoreMoneyRestrictions()) {
  1385. return true;
  1386. }
  1387. }
  1388. return Data::CanSendTexts(thread);
  1389. };
  1390. auto object = Box<ShareBox>(ShareBox::Descriptor{
  1391. .session = session,
  1392. .copyCallback = std::move(copyCallback),
  1393. .countMessagesCallback = std::move(countMessagesCallback),
  1394. .submitCallback = std::move(submitCallback),
  1395. .filterCallback = std::move(filterCallback),
  1396. .moneyRestrictionError = ShareMessageMoneyRestrictionError(),
  1397. });
  1398. *box = Ui::MakeWeak(object.data());
  1399. return object;
  1400. }
  1401. object_ptr<Ui::BoxContent> InviteLinkQrBox(
  1402. PeerData *peer,
  1403. const QString &link,
  1404. rpl::producer<QString> title,
  1405. rpl::producer<QString> about) {
  1406. return Box([=, t = std::move(title), a = std::move(about)](
  1407. not_null<Ui::GenericBox*> box) {
  1408. Ui::FillPeerQrBox(box, peer, link, std::move(a));
  1409. box->setTitle(std::move(t));
  1410. });
  1411. }
  1412. object_ptr<Ui::BoxContent> EditLinkBox(
  1413. not_null<PeerData*> peer,
  1414. const Api::InviteLink &data) {
  1415. constexpr auto kPeriod = 3600 * 24 * 30;
  1416. constexpr auto kTestModePeriod = 300;
  1417. const auto creating = data.link.isEmpty();
  1418. const auto box = std::make_shared<QPointer<Ui::GenericBox>>();
  1419. using Fields = Ui::InviteLinkFields;
  1420. const auto done = [=](Fields result) {
  1421. const auto finish = [=](Api::InviteLink finished) {
  1422. if (*box) {
  1423. if (creating) {
  1424. (*box)->getDelegate()->show(
  1425. ShowInviteLinkBox(peer, finished));
  1426. }
  1427. (*box)->closeBox();
  1428. }
  1429. };
  1430. if (creating) {
  1431. Assert(data.admin->isSelf());
  1432. const auto period = peer->session().isTestMode()
  1433. ? kTestModePeriod
  1434. : kPeriod;
  1435. peer->session().api().inviteLinks().create({
  1436. peer,
  1437. finish,
  1438. result.label,
  1439. result.expireDate,
  1440. result.usageLimit,
  1441. result.requestApproval,
  1442. { uint64(result.subscriptionCredits), period },
  1443. });
  1444. } else if (result.subscriptionCredits) {
  1445. peer->session().api().inviteLinks().editTitle(
  1446. peer,
  1447. data.admin,
  1448. result.link,
  1449. result.label,
  1450. finish);
  1451. } else {
  1452. peer->session().api().inviteLinks().edit(
  1453. peer,
  1454. data.admin,
  1455. result.link,
  1456. result.label,
  1457. result.expireDate,
  1458. result.usageLimit,
  1459. result.requestApproval,
  1460. finish);
  1461. }
  1462. };
  1463. const auto isGroup = !peer->isBroadcast();
  1464. const auto isPublic = peer->isChannel() && peer->asChannel()->isPublic();
  1465. auto object = Box([=](not_null<Ui::GenericBox*> box) {
  1466. const auto fill = isGroup
  1467. ? Fn<Ui::InviteLinkSubscriptionToggle()>(nullptr)
  1468. : [=] {
  1469. return Ui::FillCreateInviteLinkSubscriptionToggle(box, peer);
  1470. };
  1471. if (creating) {
  1472. Ui::CreateInviteLinkBox(box, fill, isGroup, isPublic, done);
  1473. } else {
  1474. Ui::EditInviteLinkBox(
  1475. box,
  1476. fill,
  1477. Fields{
  1478. .link = data.link,
  1479. .label = data.label,
  1480. .expireDate = data.expireDate,
  1481. .usageLimit = data.usageLimit,
  1482. .subscriptionCredits = int(data.subscription.credits),
  1483. .requestApproval = data.requestApproval,
  1484. .isGroup = isGroup,
  1485. .isPublic = isPublic,
  1486. },
  1487. done);
  1488. }
  1489. });
  1490. *box = Ui::MakeWeak(object.data());
  1491. return object;
  1492. }
  1493. object_ptr<Ui::BoxContent> RevokeLinkBox(
  1494. not_null<PeerData*> peer,
  1495. not_null<UserData*> admin,
  1496. const QString &link,
  1497. bool permanent) {
  1498. const auto revoke = [=](Fn<void()> &&close) {
  1499. auto &l = peer->session().api().inviteLinks();
  1500. if (permanent) {
  1501. l.revokePermanent(peer, admin, link, std::move(close));
  1502. } else {
  1503. auto done = [c = std::move(close)](const LinkData &) { c(); };
  1504. l.revoke(peer, admin, link, std::move(done));
  1505. }
  1506. };
  1507. return Ui::MakeConfirmBox({
  1508. permanent
  1509. ? tr::lng_group_invite_about_new()
  1510. : tr::lng_group_invite_revoke_about(),
  1511. revoke
  1512. });
  1513. }
  1514. object_ptr<Ui::BoxContent> DeleteLinkBox(
  1515. not_null<PeerData*> peer,
  1516. not_null<UserData*> admin,
  1517. const QString &link) {
  1518. const auto sure = [=](Fn<void()> &&close) {
  1519. peer->session().api().inviteLinks().destroy(
  1520. peer,
  1521. admin,
  1522. link,
  1523. std::move(close));
  1524. };
  1525. return Ui::MakeConfirmBox({ tr::lng_group_invite_delete_sure(), sure });
  1526. }
  1527. object_ptr<Ui::BoxContent> ShowInviteLinkBox(
  1528. not_null<PeerData*> peer,
  1529. const Api::InviteLink &link) {
  1530. const auto admin = link.admin;
  1531. const auto linkText = link.link;
  1532. const auto revoked = link.revoked;
  1533. auto updates = peer->session().api().inviteLinks().updates(
  1534. peer,
  1535. admin
  1536. ) | rpl::filter([=](const Api::InviteLinkUpdate &update) {
  1537. return (update.was == linkText);
  1538. }) | rpl::map([=](const Api::InviteLinkUpdate &update) {
  1539. return update.now ? *update.now : LinkData{ .admin = admin };
  1540. });
  1541. auto data = rpl::single(link) | rpl::then(std::move(updates));
  1542. auto initBox = [=, data = rpl::duplicate(data)](
  1543. not_null<Ui::BoxContent*> box) {
  1544. rpl::duplicate(
  1545. data
  1546. ) | rpl::start_with_next([=](const LinkData &link) {
  1547. if (ClosingLinkBox(link, revoked)) {
  1548. box->closeBox();
  1549. return;
  1550. }
  1551. const auto now = base::unixtime::now();
  1552. box->setTitle(!link.label.isEmpty()
  1553. ? rpl::single(link.label)
  1554. : link.revoked
  1555. ? tr::lng_manage_peer_link_invite()
  1556. : IsExpiredLink(link, now)
  1557. ? tr::lng_manage_peer_link_expired()
  1558. : link.permanent
  1559. ? tr::lng_manage_peer_link_permanent()
  1560. : tr::lng_manage_peer_link_invite());
  1561. }, box->lifetime());
  1562. box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
  1563. };
  1564. return Box<PeerListBox>(
  1565. std::make_unique<Controller>(
  1566. peer,
  1567. link.admin,
  1568. std::move(data),
  1569. Controller::Role::Joined),
  1570. std::move(initBox));
  1571. }
  1572. QString PrepareRequestedRowStatus(TimeId date) {
  1573. const auto now = QDateTime::currentDateTime();
  1574. const auto parsed = base::unixtime::parse(date);
  1575. const auto parsedDate = parsed.date();
  1576. const auto time = QLocale().toString(parsed.time(), QLocale::ShortFormat);
  1577. const auto generic = [&] {
  1578. return tr::lng_group_requests_status_date_time(
  1579. tr::now,
  1580. lt_date,
  1581. langDayOfMonth(parsedDate),
  1582. lt_time,
  1583. time);
  1584. };
  1585. return (parsedDate.addDays(1) < now.date())
  1586. ? generic()
  1587. : (parsedDate.addDays(1) == now.date())
  1588. ? tr::lng_group_requests_status_yesterday(tr::now, lt_time, time)
  1589. : (now.date() == parsedDate)
  1590. ? tr::lng_group_requests_status_today(tr::now, lt_time, time)
  1591. : generic();
  1592. }