edit_peer_invite_links.cpp 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  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_links.h"
  8. #include "data/data_peer.h"
  9. #include "data/data_user.h"
  10. #include "data/data_chat.h"
  11. #include "data/data_channel.h"
  12. #include "data/data_session.h"
  13. #include "main/session/session_show.h"
  14. #include "main/main_session.h"
  15. #include "api/api_invite_links.h"
  16. #include "settings/settings_credits_graphics.h"
  17. #include "ui/wrap/slide_wrap.h"
  18. #include "ui/widgets/buttons.h"
  19. #include "ui/widgets/popup_menu.h"
  20. #include "ui/painter.h"
  21. #include "ui/rect.h"
  22. #include "ui/vertical_list.h"
  23. #include "lang/lang_keys.h"
  24. #include "ui/boxes/confirm_box.h"
  25. #include "boxes/peer_list_controllers.h"
  26. #include "boxes/peers/edit_peer_invite_link.h"
  27. #include "apiwrap.h"
  28. #include "base/weak_ptr.h"
  29. #include "base/unixtime.h"
  30. #include "styles/style_info.h"
  31. #include "styles/style_layers.h" // st::boxDividerLabel
  32. #include "styles/style_menu_icons.h"
  33. #include <QtSvg/QSvgRenderer>
  34. namespace {
  35. enum class Color {
  36. Permanent,
  37. Expiring,
  38. ExpireSoon,
  39. Expired,
  40. Revoked,
  41. Subscription,
  42. Count,
  43. };
  44. using InviteLinkData = Api::InviteLink;
  45. using InviteLinksSlice = Api::PeerInviteLinks;
  46. struct InviteLinkAction {
  47. enum class Type {
  48. Copy,
  49. Share,
  50. Edit,
  51. Revoke,
  52. Delete,
  53. };
  54. QString link;
  55. Type type = Type::Copy;
  56. };
  57. class Row;
  58. using SubscriptionRightLabel = Settings::SubscriptionRightLabel;
  59. class RowDelegate {
  60. public:
  61. virtual std::optional<SubscriptionRightLabel> rightLabel(
  62. int credits) const = 0;
  63. virtual void rowUpdateRow(not_null<Row*> row) = 0;
  64. virtual void rowPaintIcon(
  65. QPainter &p,
  66. int x,
  67. int y,
  68. int size,
  69. float64 progress,
  70. Color color) = 0;
  71. };
  72. class Row final : public PeerListRow {
  73. public:
  74. Row(
  75. not_null<RowDelegate*> delegate,
  76. const InviteLinkData &data,
  77. TimeId now);
  78. void update(const InviteLinkData &data, TimeId now);
  79. void updateExpireProgress(TimeId now);
  80. [[nodiscard]] InviteLinkData data() const;
  81. [[nodiscard]] crl::time updateExpireIn() const;
  82. QString generateName() override;
  83. QString generateShortName() override;
  84. PaintRoundImageCallback generatePaintUserpicCallback(
  85. bool forceRound) override;
  86. QSize rightActionSize() const override;
  87. bool rightActionDisabled() const override;
  88. QMargins rightActionMargins() const override;
  89. void rightActionPaint(
  90. Painter &p,
  91. int x,
  92. int y,
  93. int outerWidth,
  94. bool selected,
  95. bool actionSelected) override;
  96. private:
  97. const not_null<RowDelegate*> _delegate;
  98. std::optional<SubscriptionRightLabel> _rightLabel;
  99. InviteLinkData _data;
  100. QString _status;
  101. float64 _progressTillExpire = 0.;
  102. Color _color = Color::Permanent;
  103. };
  104. [[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) {
  105. return UniqueRowIdFromString(data.link);
  106. }
  107. [[nodiscard]] float64 ComputeProgress(
  108. const InviteLinkData &link,
  109. TimeId now) {
  110. const auto startDate = link.startDate ? link.startDate : link.date;
  111. if (link.expireDate <= startDate && link.usageLimit <= 0) {
  112. return -1;
  113. }
  114. const auto expireProgress = (link.expireDate <= startDate
  115. || now <= startDate)
  116. ? 0.
  117. : (link.expireDate <= now)
  118. ? 1.
  119. : (now - startDate) / float64(link.expireDate - startDate);
  120. const auto usageProgress = (link.usageLimit <= 0 || link.usage <= 0)
  121. ? 0.
  122. : (link.usageLimit <= link.usage)
  123. ? 1.
  124. : link.usage / float64(link.usageLimit);
  125. return std::max(expireProgress, usageProgress);
  126. }
  127. [[nodiscard]] Color ComputeColor(
  128. const InviteLinkData &link,
  129. float64 progress) {
  130. return link.subscription
  131. ? Color::Subscription
  132. : link.revoked
  133. ? Color::Revoked
  134. : (progress >= 1.)
  135. ? Color::Expired
  136. : (progress >= 3 / 4.)
  137. ? Color::ExpireSoon
  138. : (progress >= 0.)
  139. ? Color::Expiring
  140. : Color::Permanent;
  141. }
  142. [[nodiscard]] QString ComputeStatus(const InviteLinkData &link, TimeId now) {
  143. const auto expired = IsExpiredLink(link, now);
  144. const auto revoked = link.revoked;
  145. auto result = link.usage
  146. ? tr::lng_group_invite_joined(tr::now, lt_count_decimal, link.usage)
  147. : (!expired && !revoked && link.usageLimit > 0)
  148. ? tr::lng_group_invite_can_join(
  149. tr::now,
  150. lt_count_decimal,
  151. link.usageLimit)
  152. : tr::lng_group_invite_no_joined(tr::now);
  153. const auto add = [&](const QString &text) {
  154. result += QString::fromUtf8(" \xE2\x80\xA2 ") + text;
  155. };
  156. if (revoked) {
  157. return result;
  158. } else if (expired) {
  159. add(tr::lng_group_invite_link_expired(tr::now));
  160. return result;
  161. }
  162. if (link.usage > 0 && link.usageLimit > link.usage) {
  163. result += ", " + tr::lng_group_invite_remaining(
  164. tr::now,
  165. lt_count_decimal,
  166. link.usageLimit - link.usage);
  167. } else if (link.usage > 0 && link.requested > 0) {
  168. result += ", " + tr::lng_group_invite_requested(
  169. tr::now,
  170. lt_count_decimal,
  171. link.requested);
  172. } else if (link.requested > 0) {
  173. result = tr::lng_group_invite_requested_full(
  174. tr::now,
  175. lt_count_decimal,
  176. link.requested);
  177. }
  178. if (link.expireDate > now) {
  179. const auto left = (link.expireDate - now);
  180. if (left >= 86400) {
  181. add(tr::lng_group_invite_days_left(
  182. tr::now,
  183. lt_count,
  184. left / 86400));
  185. } else {
  186. const auto time = base::unixtime::parse(link.expireDate).time();
  187. add(QLocale().toString(time, QLocale::LongFormat));
  188. }
  189. }
  190. return result;
  191. }
  192. object_ptr<Ui::BoxContent> DeleteAllRevokedBox(
  193. not_null<PeerData*> peer,
  194. not_null<UserData*> admin) {
  195. const auto sure = [=](Fn<void()> &&close) {
  196. peer->session().api().inviteLinks().destroyAllRevoked(
  197. peer,
  198. admin,
  199. std::move(close));
  200. };
  201. return Ui::MakeConfirmBox({
  202. tr::lng_group_invite_delete_all_sure(),
  203. sure
  204. });
  205. }
  206. [[nodiscard]] not_null<Ui::SettingsButton*> AddCreateLinkButton(
  207. not_null<Ui::VerticalLayout*> container) {
  208. return container->add(
  209. MakeCreateLinkButton(container, tr::lng_group_invite_add()),
  210. style::margins(0, st::inviteLinkCreateSkip, 0, 0));
  211. }
  212. Row::Row(
  213. not_null<RowDelegate*> delegate,
  214. const InviteLinkData &data,
  215. TimeId now)
  216. : PeerListRow(ComputeRowId(data))
  217. , _delegate(delegate)
  218. , _data(data)
  219. , _progressTillExpire(ComputeProgress(data, now))
  220. , _color(ComputeColor(data, _progressTillExpire)) {
  221. _rightLabel = _delegate->rightLabel(_data.subscription.credits);
  222. setCustomStatus(ComputeStatus(data, now));
  223. }
  224. void Row::update(const InviteLinkData &data, TimeId now) {
  225. _data = data;
  226. _rightLabel = _delegate->rightLabel(_data.subscription.credits);
  227. _progressTillExpire = ComputeProgress(data, now);
  228. _color = ComputeColor(data, _progressTillExpire);
  229. setCustomStatus(ComputeStatus(data, now));
  230. refreshName(st::inviteLinkList.item);
  231. _delegate->rowUpdateRow(this);
  232. }
  233. void Row::updateExpireProgress(TimeId now) {
  234. const auto updated = ComputeProgress(_data, now);
  235. if (base::SafeRound(_progressTillExpire * 360)
  236. != base::SafeRound(updated * 360)) {
  237. _progressTillExpire = updated;
  238. const auto color = ComputeColor(_data, _progressTillExpire);
  239. if (_color != color) {
  240. _color = color;
  241. setCustomStatus(ComputeStatus(_data, now));
  242. }
  243. _delegate->rowUpdateRow(this);
  244. }
  245. }
  246. InviteLinkData Row::data() const {
  247. return _data;
  248. }
  249. crl::time Row::updateExpireIn() const {
  250. if (_color != Color::Expiring && _color != Color::ExpireSoon) {
  251. return 0;
  252. }
  253. const auto start = _data.startDate ? _data.startDate : _data.date;
  254. if (_data.expireDate <= start) {
  255. return 0;
  256. }
  257. return base::SafeRound(
  258. (_data.expireDate - start) * crl::time(1000) / 720.);
  259. }
  260. QString Row::generateName() {
  261. if (!_data.label.isEmpty()) {
  262. return _data.label;
  263. }
  264. auto result = _data.link;
  265. return result.replace(
  266. u"https://"_q,
  267. QString()
  268. ).replace(
  269. u"t.me/+"_q,
  270. QString()
  271. ).replace(
  272. u"t.me/joinchat/"_q,
  273. QString()
  274. );
  275. }
  276. QString Row::generateShortName() {
  277. return generateName();
  278. }
  279. PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
  280. return [=](
  281. QPainter &p,
  282. int x,
  283. int y,
  284. int outerWidth,
  285. int size) {
  286. _delegate->rowPaintIcon(p, x, y, size, _progressTillExpire, _color);
  287. };
  288. }
  289. QSize Row::rightActionSize() const {
  290. if (_rightLabel) {
  291. return _rightLabel->size;
  292. }
  293. return QSize(
  294. st::inviteLinkThreeDotsIcon.width(),
  295. st::inviteLinkThreeDotsIcon.height());
  296. }
  297. bool Row::rightActionDisabled() const {
  298. return _rightLabel.has_value();
  299. }
  300. QMargins Row::rightActionMargins() const {
  301. if (_rightLabel) {
  302. return QMargins(0, 0, st::boxRowPadding.right(), 0);
  303. }
  304. return QMargins(
  305. 0,
  306. (st::inviteLinkList.item.height - rightActionSize().height()) / 2,
  307. st::inviteLinkThreeDotsSkip,
  308. 0);
  309. }
  310. void Row::rightActionPaint(
  311. Painter &p,
  312. int x,
  313. int y,
  314. int outerWidth,
  315. bool selected,
  316. bool actionSelected) {
  317. if (_rightLabel) {
  318. return _rightLabel->draw(p, x, y, st::inviteLinkList.item.height);
  319. }
  320. (actionSelected
  321. ? st::inviteLinkThreeDotsIconOver
  322. : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);
  323. }
  324. class LinksController final
  325. : public PeerListController
  326. , public RowDelegate
  327. , public base::has_weak_ptr {
  328. public:
  329. LinksController(
  330. not_null<PeerData*> peer,
  331. not_null<UserData*> admin,
  332. int count,
  333. bool revoked);
  334. [[nodiscard]] rpl::producer<int> fullCountValue() const {
  335. return _count.value();
  336. }
  337. void prepare() override;
  338. void loadMoreRows() override;
  339. void rowClicked(not_null<PeerListRow*> row) override;
  340. void rowRightActionClicked(not_null<PeerListRow*> row) override;
  341. base::unique_qptr<Ui::PopupMenu> rowContextMenu(
  342. QWidget *parent,
  343. not_null<PeerListRow*> row) override;
  344. Main::Session &session() const override;
  345. std::optional<SubscriptionRightLabel> rightLabel(int) const override;
  346. void rowUpdateRow(not_null<Row*> row) override;
  347. void rowPaintIcon(
  348. QPainter &p,
  349. int x,
  350. int y,
  351. int size,
  352. float64 progress,
  353. Color color) override;
  354. [[nodiscard]] rpl::producer<InviteLinkData> permanentFound() const {
  355. return _permanentFound.events();
  356. }
  357. private:
  358. void appendRow(const InviteLinkData &data, TimeId now);
  359. void prependRow(const InviteLinkData &data, TimeId now);
  360. void updateRow(const InviteLinkData &data, TimeId now);
  361. bool removeRow(const QString &link);
  362. void appendSlice(const InviteLinksSlice &slice);
  363. void checkExpiringTimer(not_null<Row*> row);
  364. void expiringProgressTimer();
  365. [[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
  366. QWidget *parent,
  367. not_null<PeerListRow*> row);
  368. const not_null<PeerData*> _peer;
  369. const not_null<UserData*> _admin;
  370. const bool _revoked = false;
  371. rpl::variable<int> _count;
  372. base::unique_qptr<Ui::PopupMenu> _menu;
  373. QString _offsetLink;
  374. TimeId _offsetDate = 0;
  375. bool _requesting = false;
  376. bool _allLoaded = false;
  377. rpl::event_stream<InviteLinkData> _permanentFound;
  378. base::flat_set<not_null<Row*>> _expiringRows;
  379. base::Timer _updateExpiringTimer;
  380. std::array<QImage, int(Color::Count)> _icons;
  381. rpl::lifetime _lifetime;
  382. };
  383. LinksController::LinksController(
  384. not_null<PeerData*> peer,
  385. not_null<UserData*> admin,
  386. int count,
  387. bool revoked)
  388. : _peer(peer)
  389. , _admin(admin)
  390. , _revoked(revoked)
  391. , _count(count)
  392. , _updateExpiringTimer([=] { expiringProgressTimer(); }) {
  393. style::PaletteChanged(
  394. ) | rpl::start_with_next([=] {
  395. for (auto &image : _icons) {
  396. image = QImage();
  397. }
  398. }, _lifetime);
  399. peer->session().api().inviteLinks().updates(
  400. peer,
  401. admin
  402. ) | rpl::start_with_next([=](const Api::InviteLinkUpdate &update) {
  403. const auto now = base::unixtime::now();
  404. if (!update.now || update.now->revoked != _revoked) {
  405. if (removeRow(update.was)) {
  406. delegate()->peerListRefreshRows();
  407. }
  408. } else if (update.was.isEmpty()) {
  409. if (update.now->permanent && !update.now->revoked) {
  410. _permanentFound.fire_copy(*update.now);
  411. } else {
  412. prependRow(*update.now, now);
  413. delegate()->peerListRefreshRows();
  414. }
  415. } else {
  416. updateRow(*update.now, now);
  417. }
  418. }, _lifetime);
  419. if (_revoked) {
  420. peer->session().api().inviteLinks().allRevokedDestroyed(
  421. peer,
  422. admin
  423. ) | rpl::start_with_next([=] {
  424. _requesting = false;
  425. _allLoaded = true;
  426. while (delegate()->peerListFullRowsCount()) {
  427. delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
  428. }
  429. delegate()->peerListRefreshRows();
  430. }, _lifetime);
  431. }
  432. }
  433. void LinksController::prepare() {
  434. if (!_revoked && _admin->isSelf()) {
  435. appendSlice(_peer->session().api().inviteLinks().myLinks(_peer));
  436. }
  437. if (!delegate()->peerListFullRowsCount()) {
  438. loadMoreRows();
  439. }
  440. }
  441. void LinksController::loadMoreRows() {
  442. if (_requesting || _allLoaded) {
  443. return;
  444. }
  445. _requesting = true;
  446. const auto done = [=](const InviteLinksSlice &slice) {
  447. if (!_requesting) {
  448. return;
  449. }
  450. _requesting = false;
  451. if (slice.links.empty()) {
  452. _allLoaded = true;
  453. return;
  454. }
  455. appendSlice(slice);
  456. };
  457. _peer->session().api().inviteLinks().requestMoreLinks(
  458. _peer,
  459. _admin,
  460. _offsetDate,
  461. _offsetLink,
  462. _revoked,
  463. crl::guard(this, done));
  464. }
  465. void LinksController::appendSlice(const InviteLinksSlice &slice) {
  466. const auto now = base::unixtime::now();
  467. for (const auto &link : slice.links) {
  468. if (link.permanent && !link.revoked) {
  469. _permanentFound.fire_copy(link);
  470. } else {
  471. appendRow(link, now);
  472. }
  473. _offsetLink = link.link;
  474. _offsetDate = link.date;
  475. }
  476. if (slice.links.size() >= slice.count) {
  477. _allLoaded = true;
  478. }
  479. const auto rowsCount = delegate()->peerListFullRowsCount();
  480. const auto minimalCount = _revoked ? rowsCount : (rowsCount + 1);
  481. _count = _allLoaded ? minimalCount : std::max(slice.count, minimalCount);
  482. delegate()->peerListRefreshRows();
  483. }
  484. void LinksController::rowClicked(not_null<PeerListRow*> row) {
  485. delegate()->peerListUiShow()->showBox(
  486. ShowInviteLinkBox(_peer, static_cast<Row*>(row.get())->data()));
  487. }
  488. void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
  489. delegate()->peerListShowRowMenu(row, true);
  490. }
  491. base::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(
  492. QWidget *parent,
  493. not_null<PeerListRow*> row) {
  494. auto result = createRowContextMenu(parent, row);
  495. if (result) {
  496. // First clear _menu value, so that we don't check row positions yet.
  497. base::take(_menu);
  498. // Here unique_qptr is used like a shared pointer, where
  499. // not the last destroyed pointer destroys the object, but the first.
  500. _menu = base::unique_qptr<Ui::PopupMenu>(result.get());
  501. }
  502. return result;
  503. }
  504. base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
  505. QWidget *parent,
  506. not_null<PeerListRow*> row) {
  507. const auto real = static_cast<Row*>(row.get());
  508. const auto data = real->data();
  509. const auto link = data.link;
  510. auto result = base::make_unique_q<Ui::PopupMenu>(
  511. parent,
  512. st::popupMenuWithIcons);
  513. if (data.revoked) {
  514. result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {
  515. delegate()->peerListUiShow()->showBox(
  516. DeleteLinkBox(_peer, _admin, link));
  517. }, &st::menuIconDelete);
  518. } else {
  519. result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {
  520. CopyInviteLink(delegate()->peerListUiShow(), link);
  521. }, &st::menuIconCopy);
  522. result->addAction(tr::lng_group_invite_context_share(tr::now), [=] {
  523. delegate()->peerListUiShow()->showBox(
  524. ShareInviteLinkBox(_peer, link));
  525. }, &st::menuIconShare);
  526. result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
  527. delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
  528. nullptr,
  529. link,
  530. tr::lng_group_invite_qr_title(),
  531. tr::lng_group_invite_qr_about()));
  532. }, &st::menuIconQrCode);
  533. result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
  534. delegate()->peerListUiShow()->showBox(EditLinkBox(_peer, data));
  535. }, &st::menuIconEdit);
  536. result->addAction(tr::lng_group_invite_context_revoke(tr::now), [=] {
  537. delegate()->peerListUiShow()->showBox(
  538. RevokeLinkBox(_peer, _admin, link));
  539. }, &st::menuIconRemove);
  540. }
  541. return result;
  542. }
  543. Main::Session &LinksController::session() const {
  544. return _peer->session();
  545. }
  546. void LinksController::appendRow(const InviteLinkData &data, TimeId now) {
  547. delegate()->peerListAppendRow(std::make_unique<Row>(this, data, now));
  548. }
  549. void LinksController::prependRow(const InviteLinkData &data, TimeId now) {
  550. delegate()->peerListPrependRow(std::make_unique<Row>(this, data, now));
  551. }
  552. void LinksController::updateRow(const InviteLinkData &data, TimeId now) {
  553. if (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {
  554. const auto real = static_cast<Row*>(row);
  555. real->update(data, now);
  556. checkExpiringTimer(real);
  557. delegate()->peerListUpdateRow(row);
  558. } else if (_revoked) {
  559. prependRow(data, now);
  560. delegate()->peerListRefreshRows();
  561. }
  562. }
  563. bool LinksController::removeRow(const QString &link) {
  564. const auto id = UniqueRowIdFromString(link);
  565. if (const auto row = delegate()->peerListFindRow(id)) {
  566. delegate()->peerListRemoveRow(row);
  567. return true;
  568. }
  569. return false;
  570. }
  571. void LinksController::checkExpiringTimer(not_null<Row*> row) {
  572. const auto updateIn = row->updateExpireIn();
  573. if (updateIn > 0) {
  574. _expiringRows.emplace(row);
  575. if (!_updateExpiringTimer.isActive()
  576. || updateIn < _updateExpiringTimer.remainingTime()) {
  577. _updateExpiringTimer.callOnce(updateIn);
  578. }
  579. } else {
  580. _expiringRows.remove(row);
  581. }
  582. }
  583. void LinksController::expiringProgressTimer() {
  584. const auto now = base::unixtime::now();
  585. auto minimalIn = 0;
  586. for (auto i = begin(_expiringRows); i != end(_expiringRows);) {
  587. (*i)->updateExpireProgress(now);
  588. const auto updateIn = (*i)->updateExpireIn();
  589. if (!updateIn) {
  590. i = _expiringRows.erase(i);
  591. } else {
  592. ++i;
  593. if (!minimalIn || minimalIn > updateIn) {
  594. minimalIn = updateIn;
  595. }
  596. }
  597. }
  598. if (minimalIn) {
  599. _updateExpiringTimer.callOnce(minimalIn);
  600. }
  601. }
  602. std::optional<SubscriptionRightLabel> LinksController::rightLabel(
  603. int credits) const {
  604. if (credits > 0) {
  605. return Settings::PaintSubscriptionRightLabelCallback(
  606. &session(),
  607. st::inviteLinkList.item,
  608. credits);
  609. }
  610. return std::nullopt;
  611. }
  612. void LinksController::rowUpdateRow(not_null<Row*> row) {
  613. delegate()->peerListUpdateRow(row);
  614. }
  615. void LinksController::rowPaintIcon(
  616. QPainter &p,
  617. int x,
  618. int y,
  619. int size,
  620. float64 progress,
  621. Color color) {
  622. const auto skip = st::inviteLinkIconSkip;
  623. const auto inner = size - 2 * skip;
  624. const auto bg = [&] {
  625. switch (color) {
  626. case Color::Permanent: return &st::msgFile1Bg;
  627. case Color::Expiring: return &st::msgFile2Bg;
  628. case Color::ExpireSoon: return &st::msgFile4Bg;
  629. case Color::Expired: return &st::msgFile3Bg;
  630. case Color::Revoked: return &st::windowSubTextFg;
  631. case Color::Subscription: return &st::msgFile2Bg;
  632. }
  633. Unexpected("Color in LinksController::rowPaintIcon.");
  634. }();
  635. const auto stroke = st::inviteLinkIconStroke;
  636. auto &icon = _icons[int(color)];
  637. if (icon.isNull()) {
  638. icon = QImage(
  639. QSize(inner, inner) * style::DevicePixelRatio(),
  640. QImage::Format_ARGB32_Premultiplied);
  641. icon.fill(Qt::transparent);
  642. icon.setDevicePixelRatio(style::DevicePixelRatio());
  643. auto p = QPainter(&icon);
  644. p.setPen(Qt::NoPen);
  645. p.setBrush(*bg);
  646. {
  647. auto hq = PainterHighQualityEnabler(p);
  648. const auto rect = QRect(0, 0, inner, inner)
  649. - ((color == Color::Expiring || color == Color::ExpireSoon)
  650. ? Margins(stroke)
  651. : Margins(0));
  652. p.drawEllipse(rect);
  653. }
  654. if (color == Color::Subscription) {
  655. auto svg = QSvgRenderer(u":/gui/links_subscription.svg"_q);
  656. const auto r = QRect(
  657. (inner - st::inviteLinkSubscriptionSize) / 2,
  658. (inner - st::inviteLinkSubscriptionSize) / 2,
  659. st::inviteLinkSubscriptionSize,
  660. st::inviteLinkSubscriptionSize);
  661. svg.render(&p, r);
  662. } else {
  663. (color == Color::Revoked
  664. ? st::inviteLinkRevokedIcon
  665. : st::inviteLinkIcon).paintInCenter(p, Rect(Size(inner)));
  666. }
  667. }
  668. p.drawImage(x + skip, y + skip, icon);
  669. if (progress >= 0. && progress < 1.) {
  670. auto hq = PainterHighQualityEnabler(p);
  671. auto pen = QPen((*bg)->c);
  672. pen.setWidth(stroke);
  673. pen.setCapStyle(Qt::RoundCap);
  674. p.setPen(pen);
  675. p.setBrush(Qt::NoBrush);
  676. const auto margins = .5 * stroke;
  677. p.drawArc(QRectF(x + skip, y + skip, inner, inner).marginsAdded({
  678. margins,
  679. margins,
  680. margins,
  681. margins,
  682. }), arc::kQuarterLength, arc::kFullLength * (1. - progress));
  683. }
  684. }
  685. class AdminsController final
  686. : public PeerListController
  687. , public base::has_weak_ptr {
  688. public:
  689. AdminsController(not_null<PeerData*> peer, not_null<UserData*> admin);
  690. ~AdminsController();
  691. void prepare() override;
  692. void loadMoreRows() override;
  693. void rowClicked(not_null<PeerListRow*> row) override;
  694. Main::Session &session() const override;
  695. private:
  696. void appendRow(not_null<UserData*> user, int count);
  697. const not_null<PeerData*> _peer;
  698. const not_null<UserData*> _admin;
  699. mtpRequestId _requestId = 0;
  700. };
  701. AdminsController::AdminsController(
  702. not_null<PeerData*> peer,
  703. not_null<UserData*> admin)
  704. : _peer(peer)
  705. , _admin(admin) {
  706. }
  707. AdminsController::~AdminsController() {
  708. session().api().request(base::take(_requestId)).cancel();
  709. }
  710. void AdminsController::prepare() {
  711. if (const auto chat = _peer->asChat()) {
  712. if (!chat->amCreator()) {
  713. return;
  714. }
  715. } else if (const auto channel = _peer->asChannel()) {
  716. if (!channel->amCreator()) {
  717. return;
  718. }
  719. }
  720. if (!_admin->isSelf()) {
  721. return;
  722. }
  723. _requestId = session().api().request(MTPmessages_GetAdminsWithInvites(
  724. _peer->input
  725. )).done([=](const MTPmessages_ChatAdminsWithInvites &result) {
  726. result.match([&](const MTPDmessages_chatAdminsWithInvites &data) {
  727. auto &owner = _peer->owner();
  728. owner.processUsers(data.vusers());
  729. for (const auto &admin : data.vadmins().v) {
  730. admin.match([&](const MTPDchatAdminWithInvites &data) {
  731. const auto adminId = data.vadmin_id();
  732. if (const auto user = owner.userLoaded(adminId)) {
  733. if (!user->isSelf()) {
  734. appendRow(user, data.vinvites_count().v);
  735. }
  736. }
  737. });
  738. }
  739. delegate()->peerListRefreshRows();
  740. });
  741. }).send();
  742. }
  743. void AdminsController::loadMoreRows() {
  744. }
  745. void AdminsController::rowClicked(not_null<PeerListRow*> row) {
  746. delegate()->peerListUiShow()->showBox(
  747. Box(ManageInviteLinksBox, _peer, row->peer()->asUser(), 0, 0));
  748. }
  749. Main::Session &AdminsController::session() const {
  750. return _peer->session();
  751. }
  752. void AdminsController::appendRow(not_null<UserData*> user, int count) {
  753. auto row = std::make_unique<PeerListRow>(user);
  754. row->setCustomStatus(
  755. tr::lng_group_invite_other_count(tr::now, lt_count, count));
  756. delegate()->peerListAppendRow(std::move(row));
  757. }
  758. } // namespace
  759. struct LinksList {
  760. not_null<Ui::RpWidget*> widget;
  761. not_null<LinksController*> controller;
  762. };
  763. LinksList AddLinksList(
  764. std::shared_ptr<Main::SessionShow> show,
  765. not_null<Ui::VerticalLayout*> container,
  766. not_null<PeerData*> peer,
  767. not_null<UserData*> admin,
  768. int count,
  769. bool revoked) {
  770. auto &lifetime = container->lifetime();
  771. const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
  772. show);
  773. const auto controller = lifetime.make_state<LinksController>(
  774. peer,
  775. admin,
  776. count,
  777. revoked);
  778. controller->setStyleOverrides(&st::inviteLinkList);
  779. const auto content = container->add(object_ptr<PeerListContent>(
  780. container,
  781. controller));
  782. delegate->setContent(content);
  783. controller->setDelegate(delegate);
  784. return { content, controller };
  785. }
  786. not_null<Ui::RpWidget*> AddAdminsList(
  787. std::shared_ptr<Main::SessionShow> show,
  788. not_null<Ui::VerticalLayout*> container,
  789. not_null<PeerData*> peer,
  790. not_null<UserData*> admin) {
  791. auto &lifetime = container->lifetime();
  792. const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
  793. show);
  794. const auto controller = lifetime.make_state<AdminsController>(
  795. peer,
  796. admin);
  797. controller->setStyleOverrides(&st::inviteLinkAdminsList);
  798. const auto content = container->add(object_ptr<PeerListContent>(
  799. container,
  800. controller));
  801. delegate->setContent(content);
  802. controller->setDelegate(delegate);
  803. return content;
  804. }
  805. void ManageInviteLinksBox(
  806. not_null<Ui::GenericBox*> box,
  807. not_null<PeerData*> peer,
  808. not_null<UserData*> admin,
  809. int count,
  810. int revokedCount) {
  811. const auto show = Main::MakeSessionShow(
  812. box->uiShow(),
  813. &peer->session());
  814. box->setTitle(tr::lng_group_invite_title());
  815. box->setWidth(st::boxWideWidth);
  816. const auto container = box->verticalLayout();
  817. const auto permanentFromList = box->lifetime().make_state<
  818. rpl::event_stream<InviteLinkData>
  819. >();
  820. const auto countValue = box->lifetime().make_state<rpl::variable<int>>(
  821. count);
  822. if (!admin->isSelf()) {
  823. auto status = tr::lng_group_invite_links_count(
  824. lt_count,
  825. countValue->value() | tr::to_count());
  826. AddSinglePeerRow(
  827. container,
  828. admin,
  829. std::move(status));
  830. }
  831. Ui::AddSubsectionTitle(container, tr::lng_create_permanent_link_title());
  832. AddPermanentLinkBlock(
  833. show,
  834. container,
  835. peer,
  836. admin,
  837. permanentFromList->events());
  838. Ui::AddDivider(container);
  839. auto otherHeader = (Ui::SlideWrap<>*)nullptr;
  840. if (admin->isSelf()) {
  841. const auto add = AddCreateLinkButton(container);
  842. add->setClickedCallback([=] {
  843. show->showBox(
  844. EditLinkBox(peer, InviteLinkData{ .admin = admin }));
  845. });
  846. } else {
  847. otherHeader = container->add(object_ptr<Ui::SlideWrap<>>(
  848. container,
  849. object_ptr<Ui::FlatLabel>(
  850. container,
  851. tr::lng_group_invite_other_list(),
  852. st::defaultSubsectionTitle),
  853. st::inviteLinkRevokedTitlePadding));
  854. }
  855. auto [list, controller] = AddLinksList(
  856. show,
  857. container,
  858. peer,
  859. admin,
  860. count,
  861. false);
  862. *countValue = controller->fullCountValue();
  863. controller->permanentFound(
  864. ) | rpl::start_with_next([=](InviteLinkData &&data) {
  865. permanentFromList->fire(std::move(data));
  866. }, container->lifetime());
  867. const auto dividerAbout = container->add(object_ptr<Ui::SlideWrap<>>(
  868. container,
  869. object_ptr<Ui::DividerLabel>(
  870. container,
  871. object_ptr<Ui::FlatLabel>(
  872. container,
  873. tr::lng_group_invite_add_about(),
  874. st::boxDividerLabel),
  875. st::defaultBoxDividerLabelPadding)),
  876. style::margins(0, st::inviteLinkCreateSkip, 0, 0));
  877. const auto adminsDivider = container->add(object_ptr<Ui::SlideWrap<>>(
  878. container,
  879. object_ptr<Ui::BoxContentDivider>(container)));
  880. const auto adminsHeader = container->add(object_ptr<Ui::SlideWrap<>>(
  881. container,
  882. object_ptr<Ui::FlatLabel>(
  883. container,
  884. tr::lng_group_invite_other_title(),
  885. st::defaultSubsectionTitle),
  886. st::inviteLinkRevokedTitlePadding));
  887. const auto admins = AddAdminsList(show, container, peer, admin);
  888. const auto revokedDivider = container->add(object_ptr<Ui::SlideWrap<>>(
  889. container,
  890. object_ptr<Ui::BoxContentDivider>(container)));
  891. const auto revokedHeader = container->add(object_ptr<Ui::SlideWrap<>>(
  892. container,
  893. object_ptr<Ui::FlatLabel>(
  894. container,
  895. tr::lng_group_invite_revoked_title(),
  896. st::defaultSubsectionTitle),
  897. st::inviteLinkRevokedTitlePadding));
  898. const auto revoked = AddLinksList(
  899. show,
  900. container,
  901. peer,
  902. admin,
  903. revokedCount,
  904. true).widget;
  905. const auto deleteAll = Ui::CreateChild<Ui::LinkButton>(
  906. container.get(),
  907. tr::lng_group_invite_context_delete_all(tr::now),
  908. st::defaultLinkButton);
  909. rpl::combine(
  910. revokedHeader->topValue(),
  911. container->widthValue()
  912. ) | rpl::start_with_next([=](int top, int outerWidth) {
  913. deleteAll->moveToRight(
  914. st::inviteLinkRevokedTitlePadding.left(),
  915. top + st::inviteLinkRevokedTitlePadding.top(),
  916. outerWidth);
  917. }, deleteAll->lifetime());
  918. deleteAll->setClickedCallback([=, show = box->uiShow()] {
  919. show->showBox(DeleteAllRevokedBox(peer, admin));
  920. });
  921. rpl::combine(
  922. list->heightValue(),
  923. admins->heightValue(),
  924. revoked->heightValue()
  925. ) | rpl::start_with_next([=](int list, int admins, int revoked) {
  926. if (otherHeader) {
  927. otherHeader->toggle(list > 0, anim::type::instant);
  928. }
  929. dividerAbout->toggle(!list && !otherHeader, anim::type::instant);
  930. adminsDivider->toggle(admins > 0 && list > 0, anim::type::instant);
  931. adminsHeader->toggle(admins > 0, anim::type::instant);
  932. revokedDivider->toggle(revoked > 0 && (list > 0 || admins > 0), anim::type::instant);
  933. revokedHeader->toggle(revoked > 0, anim::type::instant);
  934. deleteAll->setVisible(revoked > 0);
  935. }, revokedHeader->lifetime());
  936. box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
  937. }
  938. object_ptr<Ui::SettingsButton> MakeCreateLinkButton(
  939. not_null<QWidget*> parent,
  940. rpl::producer<QString> text) {
  941. auto result = object_ptr<Ui::SettingsButton>(
  942. parent,
  943. std::move(text),
  944. st::inviteLinkCreate);
  945. const auto raw = result.data();
  946. const auto icon = Ui::CreateChild<Ui::RpWidget>(raw);
  947. icon->setAttribute(Qt::WA_TransparentForMouseEvents);
  948. const auto size = st::inviteLinkCreateIconSize;
  949. icon->resize(size, size);
  950. raw->heightValue(
  951. ) | rpl::start_with_next([=](int height) {
  952. const auto &st = st::inviteLinkList.item;
  953. icon->move(
  954. st.photoPosition.x() + (st.photoSize - size) / 2,
  955. (height - size) / 2);
  956. }, icon->lifetime());
  957. icon->paintRequest(
  958. ) | rpl::start_with_next([=] {
  959. auto p = QPainter(icon);
  960. p.setPen(Qt::NoPen);
  961. p.setBrush(st::windowBgActive);
  962. const auto rect = icon->rect();
  963. {
  964. auto hq = PainterHighQualityEnabler(p);
  965. p.drawEllipse(rect);
  966. }
  967. st::inviteLinkCreateIcon.paintInCenter(p, rect);
  968. }, icon->lifetime());
  969. return result;
  970. }