info_bot_starref_common.cpp 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  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 "info/bot/starref/info_bot_starref_common.h"
  8. #include "apiwrap.h"
  9. #include "boxes/peers/replace_boost_box.h" // CreateUserpicsTransfer.
  10. #include "boxes/send_credits_box.h" // Ui::CreditsEmoji.
  11. #include "chat_helpers/stickers_lottie.h"
  12. #include "core/ui_integration.h"
  13. #include "data/stickers/data_custom_emoji.h"
  14. #include "data/data_channel.h"
  15. #include "data/data_document.h"
  16. #include "data/data_session.h"
  17. #include "history/view/media/history_view_sticker.h"
  18. #include "history/view/media/history_view_sticker_player.h"
  19. #include "lang/lang_keys.h"
  20. #include "main/main_session.h"
  21. #include "settings/settings_common.h"
  22. #include "ui/boxes/confirm_box.h"
  23. #include "ui/controls/userpic_button.h"
  24. #include "ui/controls/who_reacted_context_action.h"
  25. #include "ui/dynamic_image.h"
  26. #include "ui/dynamic_thumbnails.h"
  27. #include "ui/layers/generic_box.h"
  28. #include "ui/widgets/buttons.h"
  29. #include "ui/widgets/labels.h"
  30. #include "ui/widgets/popup_menu.h"
  31. #include "ui/wrap/padding_wrap.h"
  32. #include "ui/wrap/table_layout.h"
  33. #include "ui/wrap/vertical_layout.h"
  34. #include "ui/text/text_utilities.h"
  35. #include "ui/new_badges.h"
  36. #include "ui/painter.h"
  37. #include "ui/vertical_list.h"
  38. #include "styles/style_boxes.h"
  39. #include "styles/style_chat.h"
  40. #include "styles/style_chat_helpers.h"
  41. #include "styles/style_dialogs.h"
  42. #include "styles/style_giveaway.h"
  43. #include "styles/style_layers.h"
  44. #include "styles/style_premium.h"
  45. #include "styles/style_settings.h"
  46. #include <QtWidgets/QApplication>
  47. namespace Info::BotStarRef {
  48. namespace {
  49. void ConnectStarRef(
  50. not_null<UserData*> bot,
  51. not_null<PeerData*> peer,
  52. Fn<void(ConnectedBot)> done,
  53. Fn<void(const QString &)> fail) {
  54. bot->session().api().request(MTPpayments_ConnectStarRefBot(
  55. peer->input,
  56. bot->inputUser
  57. )).done([=](const MTPpayments_ConnectedStarRefBots &result) {
  58. const auto parsed = Parse(&bot->session(), result);
  59. if (parsed.empty()) {
  60. fail(u"EMPTY"_q);
  61. } else {
  62. done(parsed.front());
  63. }
  64. }).fail([=](const MTP::Error &error) {
  65. fail(error.type());
  66. }).send();
  67. }
  68. [[nodiscard]] object_ptr<Ui::RpWidget> CreateLinkIcon(
  69. not_null<QWidget*> parent,
  70. not_null<UserData*> bot,
  71. int users) {
  72. auto result = object_ptr<Ui::RpWidget>(parent);
  73. const auto raw = result.data();
  74. struct State {
  75. not_null<DocumentData*> icon;
  76. std::shared_ptr<Data::DocumentMedia> media;
  77. std::shared_ptr<HistoryView::StickerPlayer> player;
  78. int counterWidth = 0;
  79. };
  80. const auto outerSide = st::starrefLinkThumbOuter;
  81. const auto outerSkip = (outerSide - st::starrefLinkThumbInner) / 2;
  82. const auto innerSide = (outerSide - 2 * outerSkip);
  83. const auto add = st::starrefLinkCountAdd;
  84. const auto outer = QSize(outerSide, outerSide + add);
  85. const auto inner = QSize(innerSide, innerSide);
  86. const auto state = raw->lifetime().make_state<State>(State{
  87. .icon = ChatHelpers::GenerateLocalTgsSticker(
  88. &bot->session(),
  89. u"starref_link"_q),
  90. });
  91. state->icon->overrideEmojiUsesTextColor(true);
  92. state->media = state->icon->createMediaView();
  93. state->player = std::make_unique<HistoryView::LottiePlayer>(
  94. ChatHelpers::LottiePlayerFromDocument(
  95. state->media.get(),
  96. ChatHelpers::StickerLottieSize::MessageHistory,
  97. inner,
  98. Lottie::Quality::High));
  99. const auto player = state->player.get();
  100. player->setRepaintCallback([=] { raw->update(); });
  101. const auto text = users
  102. ? Lang::FormatCountToShort(users).string
  103. : QString();
  104. const auto length = st::starrefLinkCountFont->width(text);
  105. const auto contents = length + st::starrefLinkCountIcon.width();
  106. const auto delta = (outer.width() - contents) / 2;
  107. const auto badge = QRect(
  108. delta,
  109. outer.height() - st::starrefLinkCountFont->height - st::lineWidth,
  110. outer.width() - 2 * delta,
  111. st::starrefLinkCountFont->height);
  112. const auto badgeRect = badge.marginsAdded(st::starrefLinkCountPadding);
  113. raw->paintRequest() | rpl::start_with_next([=] {
  114. auto p = QPainter(raw);
  115. p.setPen(Qt::NoPen);
  116. p.setBrush(st::windowBgActive);
  117. auto hq = PainterHighQualityEnabler(p);
  118. const auto left = (raw->width() - outer.width()) / 2;
  119. p.drawEllipse(left, 0, outerSide, outerSide);
  120. if (!text.isEmpty()) {
  121. const auto rect = badgeRect.translated(left, 0);
  122. const auto textRect = badge.translated(left, 0);
  123. const auto radius = st::starrefLinkCountFont->height / 2.;
  124. p.setPen(st::historyPeerUserpicFg);
  125. p.setBrush(st::historyPeer2UserpicBg2);
  126. p.drawRoundedRect(rect, radius, radius);
  127. p.setFont(st::starrefLinkCountFont);
  128. const auto shift = QPoint(
  129. st::starrefLinkCountIcon.width(),
  130. st::starrefLinkCountFont->ascent);
  131. st::starrefLinkCountIcon.paint(
  132. p,
  133. textRect.topLeft() + st::starrefLinkCountIconPosition,
  134. raw->width());
  135. p.drawText(textRect.topLeft() + shift, text);
  136. }
  137. if (player->ready()) {
  138. const auto now = crl::now();
  139. const auto color = st::windowFgActive->c;
  140. auto info = player->frame(inner, color, false, now, false);
  141. p.drawImage(
  142. QRect(QPoint(left + outerSkip, outerSkip), inner),
  143. info.image);
  144. if (info.index + 1 < player->framesCount()) {
  145. player->markFrameShown();
  146. }
  147. }
  148. }, raw->lifetime());
  149. raw->resize(outer);
  150. return result;
  151. }
  152. void ChooseRecipient(
  153. not_null<Ui::RpWidget*> button,
  154. const std::vector<not_null<PeerData*>> &list,
  155. not_null<PeerData*> now,
  156. Fn<void(not_null<PeerData*>)> done) {
  157. const auto menu = Ui::CreateChild<Ui::PopupMenu>(
  158. button,
  159. st::starrefPopupMenu);
  160. struct Entry {
  161. not_null<Ui::WhoReactedEntryAction*> action;
  162. std::shared_ptr<Ui::DynamicImage> userpic;
  163. };
  164. auto actions = std::make_shared<std::vector<Entry>>();
  165. actions->reserve(list.size());
  166. for (const auto &peer : list) {
  167. auto view = peer->createUserpicView();
  168. auto action = base::make_unique_q<Ui::WhoReactedEntryAction>(
  169. menu->menu(),
  170. Data::ReactedMenuFactory(&list.front()->session()),
  171. menu->menu()->st(),
  172. Ui::WhoReactedEntryData());
  173. const auto index = int(actions->size());
  174. actions->push_back({ action.get(), Ui::MakeUserpicThumbnail(peer) });
  175. const auto updateUserpic = [=] {
  176. const auto size = st::defaultWhoRead.photoSize;
  177. actions->at(index).action->setData({
  178. .text = peer->name(),
  179. .date = (peer->isSelf()
  180. ? tr::lng_group_call_join_as_personal(tr::now)
  181. : peer->isUser()
  182. ? tr::lng_status_bot(tr::now)
  183. : peer->isBroadcast()
  184. ? tr::lng_channel_status(tr::now)
  185. : tr::lng_group_status(tr::now)),
  186. .type = (peer == now
  187. ? Ui::WhoReactedType::RefRecipientNow
  188. : Ui::WhoReactedType::RefRecipient),
  189. .userpic = actions->at(index).userpic->image(size),
  190. .callback = [=] { done(peer); },
  191. });
  192. };
  193. actions->back().userpic->subscribeToUpdates(updateUserpic);
  194. menu->addAction(std::move(action));
  195. updateUserpic();
  196. }
  197. menu->popup(button->mapToGlobal(QPoint(button->width() / 2, 0)));
  198. }
  199. } // namespace
  200. QString FormatCommission(ushort commission) {
  201. return QString::number(commission / 10.) + '%';
  202. }
  203. QString FormatProgramDuration(int durationMonths) {
  204. return !durationMonths
  205. ? tr::lng_star_ref_duration_forever(tr::now)
  206. : (durationMonths < 12)
  207. ? tr::lng_months(tr::now, lt_count, durationMonths)
  208. : tr::lng_years(tr::now, lt_count, durationMonths / 12);
  209. }
  210. rpl::producer<TextWithEntities> FormatForProgramDuration(
  211. int durationMonths) {
  212. return !durationMonths
  213. ? tr::lng_star_ref_one_about_for_forever(Ui::Text::RichLangValue)
  214. : (durationMonths < 12)
  215. ? tr::lng_star_ref_one_about_for_months(
  216. lt_count,
  217. rpl::single(durationMonths * 1.),
  218. Ui::Text::RichLangValue)
  219. : tr::lng_star_ref_one_about_for_years(
  220. lt_count,
  221. rpl::single((durationMonths / 12) * 1.),
  222. Ui::Text::RichLangValue);
  223. }
  224. not_null<Ui::AbstractButton*> AddViewListButton(
  225. not_null<Ui::VerticalLayout*> parent,
  226. rpl::producer<QString> title,
  227. rpl::producer<QString> subtitle,
  228. bool newBadge) {
  229. const auto &stLabel = st::defaultFlatLabel;
  230. const auto iconSize = st::settingsPremiumIconDouble.size();
  231. const auto &titlePadding = st::settingsPremiumRowTitlePadding;
  232. const auto &descriptionPadding = st::settingsPremiumRowAboutPadding;
  233. const auto button = Ui::CreateChild<Ui::SettingsButton>(
  234. parent,
  235. rpl::single(QString()));
  236. button->show();
  237. const auto label = parent->add(
  238. object_ptr<Ui::FlatLabel>(
  239. parent,
  240. std::move(title) | Ui::Text::ToBold(),
  241. stLabel),
  242. titlePadding);
  243. label->setAttribute(Qt::WA_TransparentForMouseEvents);
  244. const auto description = parent->add(
  245. object_ptr<Ui::FlatLabel>(
  246. parent,
  247. std::move(subtitle),
  248. st::boxDividerLabel),
  249. descriptionPadding);
  250. description->setAttribute(Qt::WA_TransparentForMouseEvents);
  251. if (newBadge) {
  252. Ui::NewBadge::AddAfterLabel(parent, label);
  253. }
  254. const auto dummy = Ui::CreateChild<Ui::AbstractButton>(parent);
  255. dummy->setAttribute(Qt::WA_TransparentForMouseEvents);
  256. dummy->show();
  257. parent->sizeValue(
  258. ) | rpl::start_with_next([=](const QSize &s) {
  259. dummy->resize(s.width(), iconSize.height());
  260. }, dummy->lifetime());
  261. button->geometryValue(
  262. ) | rpl::start_with_next([=](const QRect &r) {
  263. dummy->moveToLeft(0, r.y() + (r.height() - iconSize.height()) / 2);
  264. }, dummy->lifetime());
  265. ::Settings::AddButtonIcon(dummy, st::settingsButton, {
  266. .icon = &st::settingsStarRefEarnStars,
  267. .backgroundBrush = st::premiumIconBg3,
  268. });
  269. rpl::combine(
  270. parent->widthValue(),
  271. label->heightValue(),
  272. description->heightValue()
  273. ) | rpl::start_with_next([=,
  274. topPadding = titlePadding,
  275. bottomPadding = descriptionPadding](
  276. int width,
  277. int topHeight,
  278. int bottomHeight) {
  279. button->resize(
  280. width,
  281. topPadding.top()
  282. + topHeight
  283. + topPadding.bottom()
  284. + bottomPadding.top()
  285. + bottomHeight
  286. + bottomPadding.bottom());
  287. }, button->lifetime());
  288. label->topValue(
  289. ) | rpl::start_with_next([=, padding = titlePadding.top()](int top) {
  290. button->moveToLeft(0, top - padding);
  291. }, button->lifetime());
  292. const auto arrow = Ui::CreateChild<Ui::IconButton>(
  293. button,
  294. st::backButton);
  295. arrow->setIconOverride(
  296. &st::settingsPremiumArrow,
  297. &st::settingsPremiumArrowOver);
  298. arrow->setAttribute(Qt::WA_TransparentForMouseEvents);
  299. button->sizeValue(
  300. ) | rpl::start_with_next([=](const QSize &s) {
  301. const auto &point = st::settingsPremiumArrowShift;
  302. arrow->moveToRight(
  303. -point.x(),
  304. point.y() + (s.height() - arrow->height()) / 2);
  305. }, arrow->lifetime());
  306. return button;
  307. }
  308. not_null<Ui::RoundButton*> AddFullWidthButton(
  309. not_null<Ui::BoxContent*> box,
  310. rpl::producer<QString> text,
  311. Fn<void()> callback,
  312. const style::RoundButton *stOverride) {
  313. const auto &boxSt = box->getDelegate()->style();
  314. const auto result = box->addButton(
  315. std::move(text),
  316. std::move(callback),
  317. stOverride ? *stOverride : boxSt.button);
  318. rpl::combine(
  319. box->widthValue(),
  320. result->widthValue()
  321. ) | rpl::start_with_next([=](int width, int buttonWidth) {
  322. const auto correct = width
  323. - boxSt.buttonPadding.left()
  324. - boxSt.buttonPadding.right();
  325. if (correct > 0 && buttonWidth != correct) {
  326. result->resizeToWidth(correct);
  327. result->moveToLeft(
  328. boxSt.buttonPadding.left(),
  329. boxSt.buttonPadding.top(),
  330. width);
  331. }
  332. }, result->lifetime());
  333. return result;
  334. }
  335. void AddFullWidthButtonFooter(
  336. not_null<Ui::BoxContent*> box,
  337. not_null<Ui::RpWidget*> button,
  338. rpl::producer<TextWithEntities> text) {
  339. const auto footer = Ui::CreateChild<Ui::FlatLabel>(
  340. button->parentWidget(),
  341. std::move(text),
  342. st::starrefJoinFooter);
  343. footer->setTryMakeSimilarLines(true);
  344. button->geometryValue() | rpl::start_with_next([=](QRect geometry) {
  345. footer->resizeToWidth(geometry.width());
  346. const auto &st = box->getDelegate()->style();
  347. const auto top = geometry.y() + geometry.height();
  348. const auto available = st.buttonPadding.bottom();
  349. footer->moveToLeft(
  350. geometry.left(),
  351. top + (available - footer->height()) / 2);
  352. }, footer->lifetime());
  353. }
  354. object_ptr<Ui::AbstractButton> MakeLinkLabel(
  355. not_null<QWidget*> parent,
  356. const QString &link) {
  357. const auto text = link.startsWith(u"https://"_q)
  358. ? link.mid(8)
  359. : link.startsWith(u"http://"_q)
  360. ? link.mid(7)
  361. : link;
  362. const auto margins = st::dialogsFilter.textMargins;
  363. const auto height = st::dialogsFilter.heightMin;
  364. const auto skip = margins.left();
  365. auto result = object_ptr<Ui::AbstractButton>(parent);
  366. const auto raw = result.data();
  367. raw->resize(height, height);
  368. raw->paintRequest() | rpl::start_with_next([=] {
  369. auto p = QPainter(raw);
  370. auto hq = PainterHighQualityEnabler(p);
  371. p.setPen(Qt::NoPen);
  372. p.setBrush(st::dialogsFilter.textBg);
  373. const auto radius = st::roundRadiusLarge;
  374. p.drawRoundedRect(0, 0, raw->width(), height, radius, radius);
  375. const auto font = st::dialogsFilter.style.font;
  376. p.setPen(st::dialogsFilter.textFg);
  377. p.setFont(font);
  378. const auto available = raw->width() - skip * 2;
  379. p.drawText(
  380. QRect(skip, margins.top(), available, font->height),
  381. style::al_top,
  382. font->elided(link, available));
  383. }, raw->lifetime());
  384. return result;
  385. }
  386. object_ptr<Ui::BoxContent> StarRefLinkBox(
  387. ConnectedBot row,
  388. not_null<PeerData*> peer) {
  389. return Box([=](not_null<Ui::GenericBox*> box) {
  390. const auto show = box->uiShow();
  391. const auto bot = row.bot;
  392. const auto program = row.state.program;
  393. box->setStyle(st::starrefFooterBox);
  394. box->setNoContentMargin(true);
  395. box->addTopButton(st::boxTitleClose, [=] {
  396. box->closeBox();
  397. });
  398. box->addRow(
  399. CreateLinkIcon(box, bot, row.state.users),
  400. st::boxRowPadding + st::starrefJoinUserpicsPadding);
  401. box->addRow(
  402. object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
  403. box,
  404. object_ptr<Ui::FlatLabel>(
  405. box,
  406. tr::lng_star_ref_link_title(),
  407. st::boxTitle)),
  408. st::boxRowPadding + st::starrefJoinTitlePadding);
  409. box->addRow(
  410. object_ptr<Ui::FlatLabel>(
  411. box,
  412. (peer->isSelf()
  413. ? tr::lng_star_ref_link_about_user
  414. : peer->isUser()
  415. ? tr::lng_star_ref_link_about_user
  416. : tr::lng_star_ref_link_about_channel)(
  417. lt_amount,
  418. rpl::single(Ui::Text::Bold(
  419. FormatCommission(program.commission))),
  420. lt_app,
  421. rpl::single(Ui::Text::Bold(bot->name())),
  422. lt_duration,
  423. FormatForProgramDuration(program.durationMonths),
  424. Ui::Text::WithEntities),
  425. st::starrefCenteredText),
  426. st::boxRowPadding);
  427. Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3);
  428. box->addRow(
  429. object_ptr<Ui::FlatLabel>(
  430. box,
  431. tr::lng_star_ref_link_recipient(),
  432. st::starrefCenteredText));
  433. Ui::AddSkip(box->verticalLayout());
  434. box->addRow(object_ptr<Ui::AbstractButton>::fromRaw(
  435. MakePeerBubbleButton(box, peer).release()
  436. ))->setAttribute(Qt::WA_TransparentForMouseEvents);
  437. Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);
  438. const auto preview = box->addRow(MakeLinkLabel(box, row.state.link));
  439. Ui::AddSkip(box->verticalLayout());
  440. const auto copy = [=](bool close) {
  441. return [=] {
  442. QApplication::clipboard()->setText(row.state.link);
  443. box->uiShow()->showToast(tr::lng_username_copied(tr::now));
  444. if (close) {
  445. box->closeBox();
  446. }
  447. };
  448. };
  449. preview->setClickedCallback(copy(false));
  450. const auto button = AddFullWidthButton(
  451. box,
  452. tr::lng_star_ref_link_copy(),
  453. copy(true),
  454. &st::starrefCopyButton);
  455. const auto name = TextWithEntities{ bot->name() };
  456. AddFullWidthButtonFooter(
  457. box,
  458. button,
  459. (row.state.users > 0
  460. ? tr::lng_star_ref_link_copy_users(
  461. lt_count,
  462. rpl::single(row.state.users * 1.),
  463. lt_app,
  464. rpl::single(name),
  465. Ui::Text::WithEntities)
  466. : tr::lng_star_ref_link_copy_none(
  467. lt_app,
  468. rpl::single(name),
  469. Ui::Text::WithEntities)));
  470. });
  471. }
  472. object_ptr<Ui::BoxContent> JoinStarRefBox(
  473. ConnectedBot row,
  474. not_null<PeerData*> initialRecipient,
  475. std::vector<not_null<PeerData*>> recipients,
  476. Fn<void(ConnectedBotState)> done) {
  477. Expects(row.bot->isUser());
  478. return Box([=](not_null<Ui::GenericBox*> box) {
  479. const auto show = box->uiShow();
  480. const auto bot = row.bot;
  481. const auto program = row.state.program;
  482. auto list = recipients;
  483. if (!list.empty()) {
  484. list.erase(ranges::remove(list, bot), end(list));
  485. if (!ranges::contains(list, initialRecipient)) {
  486. list.insert(begin(list), initialRecipient);
  487. }
  488. }
  489. box->setStyle(st::starrefFooterBox);
  490. box->setNoContentMargin(true);
  491. box->addTopButton(st::boxTitleClose, [=] {
  492. box->closeBox();
  493. });
  494. struct State {
  495. rpl::variable<not_null<PeerData*>> recipient;
  496. QPointer<Ui::GenericBox> weak;
  497. bool sent = false;
  498. };
  499. const auto state = std::make_shared<State>(State{
  500. .recipient = initialRecipient,
  501. .weak = box.get(),
  502. });
  503. const auto userpicsWrap = box->addRow(
  504. object_ptr<Ui::VerticalLayout>(box),
  505. QMargins());
  506. state->recipient.value(
  507. ) | rpl::start_with_next([=](not_null<PeerData*> recipient) {
  508. while (userpicsWrap->count()) {
  509. delete userpicsWrap->widgetAt(0);
  510. }
  511. userpicsWrap->add(
  512. CreateUserpicsTransfer(
  513. box,
  514. rpl::single(std::vector{ not_null<PeerData*>(bot) }),
  515. recipient,
  516. UserpicsTransferType::StarRefJoin),
  517. st::boxRowPadding + st::starrefJoinUserpicsPadding);
  518. userpicsWrap->resizeToWidth(box->width());
  519. }, box->lifetime());
  520. box->addRow(
  521. object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
  522. box,
  523. object_ptr<Ui::FlatLabel>(
  524. box,
  525. tr::lng_star_ref_title(),
  526. st::boxTitle)),
  527. st::boxRowPadding + st::starrefJoinTitlePadding);
  528. box->addRow(
  529. object_ptr<Ui::FlatLabel>(
  530. box,
  531. tr::lng_star_ref_one_about(
  532. lt_app,
  533. rpl::single(Ui::Text::Bold(bot->name())),
  534. lt_amount,
  535. rpl::single(Ui::Text::Bold(
  536. FormatCommission(program.commission))),
  537. lt_duration,
  538. FormatForProgramDuration(program.durationMonths),
  539. Ui::Text::WithEntities),
  540. st::starrefCenteredText),
  541. st::boxRowPadding);
  542. Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3);
  543. if (const auto average = program.revenuePerUser) {
  544. const auto layout = box->verticalLayout();
  545. const auto session = &initialRecipient->session();
  546. auto text = Ui::Text::Colorized(Ui::CreditsEmoji(session));
  547. text.append(Lang::FormatStarsAmountRounded(average));
  548. layout->add(
  549. object_ptr<Ui::FlatLabel>(
  550. box,
  551. tr::lng_star_ref_one_daily_revenue(
  552. lt_amount,
  553. rpl::single(
  554. Ui::Text::Wrapped(text, EntityType::Bold)),
  555. Ui::Text::WithEntities),
  556. st::starrefRevenueText,
  557. st::defaultPopupMenu,
  558. Core::TextContext({ .session = session })),
  559. st::boxRowPadding);
  560. Ui::AddSkip(layout, st::defaultVerticalListSkip);
  561. }
  562. if (!list.empty()) {
  563. struct Name {
  564. not_null<PeerData*> peer;
  565. QString name;
  566. };
  567. auto names = ranges::views::transform(list, [](auto peer) {
  568. const auto name = TextUtilities::NameSortKey(peer->name());
  569. return Name{ peer, name };
  570. }) | ranges::to_vector;
  571. ranges::sort(names, ranges::less(), &Name::name);
  572. list = ranges::views::transform(names, &Name::peer)
  573. | ranges::to_vector;
  574. box->addRow(
  575. object_ptr<Ui::FlatLabel>(
  576. box,
  577. tr::lng_star_ref_link_recipient(),
  578. st::starrefCenteredText));
  579. Ui::AddSkip(box->verticalLayout());
  580. const auto recipientWrap = box->addRow(
  581. object_ptr<Ui::VerticalLayout>(box),
  582. QMargins());
  583. state->recipient.value(
  584. ) | rpl::start_with_next([=](not_null<PeerData*> recipient) {
  585. while (recipientWrap->count()) {
  586. delete recipientWrap->widgetAt(0);
  587. }
  588. const auto selectable = (list.size() > 1);
  589. const auto bgOverride = selectable
  590. ? &st::lightButtonBgOver
  591. : nullptr;
  592. const auto right = selectable
  593. ? Ui::CreateChild<Ui::RpWidget>(recipientWrap)
  594. : nullptr;
  595. if (right) {
  596. const auto skip = st::chatGiveawayPeerPadding.right();
  597. const auto icon = &st::starrefRecipientArrow;
  598. const auto height = st::chatGiveawayPeerSize
  599. - st::chatGiveawayPeerPadding.top() * 2;
  600. right->resize(skip + icon->width(), height);
  601. right->paintRequest() | rpl::start_with_next([=] {
  602. auto p = QPainter(right);
  603. icon->paint(
  604. p,
  605. skip,
  606. (height - icon->height()) / 2,
  607. right->width());
  608. }, right->lifetime());
  609. }
  610. const auto button = recipientWrap->add(
  611. object_ptr<Ui::AbstractButton>::fromRaw(
  612. MakePeerBubbleButton(
  613. box,
  614. recipient,
  615. right,
  616. bgOverride).release()),
  617. st::boxRowPadding);
  618. recipientWrap->resizeToWidth(box->width());
  619. if (!selectable) {
  620. button->setAttribute(Qt::WA_TransparentForMouseEvents);
  621. return;
  622. }
  623. button->setClickedCallback([=] {
  624. const auto callback = [=](not_null<PeerData*> peer) {
  625. state->recipient = peer;
  626. };
  627. ChooseRecipient(
  628. button,
  629. list,
  630. state->recipient.current(),
  631. crl::guard(button, callback));
  632. });
  633. }, box->lifetime());
  634. }
  635. const auto send = [=] {
  636. if (state->sent) {
  637. return;
  638. }
  639. state->sent = true;
  640. const auto recipient = state->recipient.current();
  641. ConnectStarRef(bot->asUser(), recipient, [=](ConnectedBot info) {
  642. if (recipient == initialRecipient) {
  643. if (const auto onstack = done) {
  644. onstack(info.state);
  645. }
  646. }
  647. show->show(StarRefLinkBox(info, recipient));
  648. if (const auto strong = state->weak.data()) {
  649. strong->closeBox();
  650. }
  651. }, [=](const QString &error) {
  652. state->sent = false;
  653. show->showToast(u"Failed: "_q + error);
  654. });
  655. };
  656. const auto button = AddFullWidthButton(
  657. box,
  658. tr::lng_star_ref_one_join(),
  659. send);
  660. AddFullWidthButtonFooter(
  661. box,
  662. button,
  663. tr::lng_star_ref_one_join_text(
  664. lt_terms,
  665. tr::lng_star_ref_button_link(
  666. ) | Ui::Text::ToLink(tr::lng_star_ref_tos_url(tr::now)),
  667. Ui::Text::WithEntities));
  668. });
  669. }
  670. object_ptr<Ui::BoxContent> ConfirmEndBox(Fn<void()> finish) {
  671. return Box([=](not_null<Ui::GenericBox*> box) {
  672. box->setTitle(tr::lng_star_ref_warning_title());
  673. const auto skip = st::defaultVerticalListSkip;
  674. const auto margins = st::boxRowPadding + QMargins(0, 0, 0, skip);
  675. box->addRow(
  676. object_ptr<Ui::FlatLabel>(
  677. box,
  678. tr::lng_star_ref_warning_if_end(Ui::Text::RichLangValue),
  679. st::boxLabel),
  680. margins);
  681. const auto addPoint = [&](tr::phrase<> text) {
  682. const auto padded = box->addRow(
  683. object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
  684. box,
  685. object_ptr<Ui::FlatLabel>(
  686. box,
  687. text(Ui::Text::RichLangValue),
  688. st::blockUserConfirmation),
  689. QMargins(st::boxTextFont->height, 0, 0, 0)),
  690. margins);
  691. padded->paintRequest() | rpl::start_with_next([=] {
  692. auto p = QPainter(padded);
  693. auto hq = PainterHighQualityEnabler(p);
  694. const auto size = st::starrefEndBulletSize;
  695. const auto top = st::starrefEndBulletTop;
  696. p.setBrush(st::windowFg);
  697. p.setPen(Qt::NoPen);
  698. p.drawEllipse(0, top, size, size);
  699. }, padded->lifetime());
  700. };
  701. addPoint(tr::lng_star_ref_warning_if_end1);
  702. addPoint(tr::lng_star_ref_warning_if_end2);
  703. addPoint(tr::lng_star_ref_warning_if_end3);
  704. const auto done = [=] {
  705. box->closeBox();
  706. finish();
  707. };
  708. box->addButton(
  709. tr::lng_star_ref_warning_end(),
  710. done,
  711. st::attentionBoxButton);
  712. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  713. });
  714. }
  715. void ResolveRecipients(
  716. not_null<Main::Session*> session,
  717. Fn<void(std::vector<not_null<PeerData*>>)> done) {
  718. struct State {
  719. not_null<Main::Session*> session;
  720. std::vector<not_null<PeerData*>> list;
  721. Fn<void(std::vector<not_null<PeerData*>>)> done;
  722. };
  723. const auto state = std::make_shared<State>(State{
  724. .session = session,
  725. .done = std::move(done),
  726. });
  727. const auto finish1 = [state](const MTPmessages_Chats &result) {
  728. const auto already = int(state->list.size());
  729. const auto session = state->session;
  730. result.match([&](const auto &data) {
  731. const auto &list = data.vchats().v;
  732. state->list.reserve(list.size() + (already ? already : 1));
  733. if (!already) {
  734. state->list.push_back(session->user());
  735. }
  736. for (const auto &chat : list) {
  737. const auto peer = session->data().processChat(chat);
  738. if (const auto channel = peer->asBroadcast()) {
  739. if (channel->canPostMessages()) {
  740. state->list.push_back(channel);
  741. }
  742. }
  743. }
  744. if (already) {
  745. base::take(state->done)(base::take(state->list));
  746. }
  747. });
  748. };
  749. const auto finish2 = [state](const MTPVector<MTPUser> &result) {
  750. const auto already = int(state->list.size());
  751. const auto session = state->session;
  752. const auto &list = result.v;
  753. state->list.reserve(list.size() + (already ? already : 1));
  754. if (!already) {
  755. state->list.push_back(session->user());
  756. }
  757. for (const auto &user : list) {
  758. state->list.push_back(session->data().processUser(user));
  759. }
  760. if (already) {
  761. base::take(state->done)(base::take(state->list));
  762. }
  763. };
  764. session->api().request(MTPchannels_GetAdminedPublicChannels(
  765. MTP_flags(0)
  766. )).done(finish1).fail([=] {
  767. finish1(MTP_messages_chats(MTP_vector<MTPChat>(0)));
  768. }).send();
  769. state->session->api().request(MTPbots_GetAdminedBots(
  770. )).done(finish2).fail([=] {
  771. finish2(MTP_vector<MTPUser>(0));
  772. }).send();
  773. }
  774. std::unique_ptr<Ui::AbstractButton> MakePeerBubbleButton(
  775. not_null<QWidget*> parent,
  776. not_null<PeerData*> peer,
  777. Ui::RpWidget *right,
  778. const style::color *bgOverride) {
  779. class Button final : public Ui::AbstractButton {
  780. public:
  781. Button(QWidget *parent, not_null<int*> innerWidth)
  782. : AbstractButton(parent)
  783. , _innerWidth(innerWidth) {
  784. }
  785. void mouseMoveEvent(QMouseEvent *e) override {
  786. const auto inner = *_innerWidth;
  787. const auto skip = (width() - inner) / 2;
  788. const auto p = e->pos();
  789. const auto over = QRect(skip, 0, inner, height()).contains(p);
  790. setOver(over, StateChangeSource::ByHover);
  791. }
  792. private:
  793. const not_null<int*> _innerWidth;
  794. };
  795. auto ownedWidth = std::make_unique<int>();
  796. const auto width = ownedWidth.get();
  797. auto result = std::make_unique<Button>(parent, width);
  798. result->lifetime().add([moved = std::move(ownedWidth)] {});
  799. const auto size = st::chatGiveawayPeerSize;
  800. const auto padding = st::chatGiveawayPeerPadding;
  801. const auto raw = result.get();
  802. const auto name = raw->lifetime().make_state<Ui::FlatLabel>(
  803. raw,
  804. rpl::single(peer->name()),
  805. st::botEmojiStatusName);
  806. const auto userpic = raw->lifetime().make_state<Ui::UserpicButton>(
  807. raw,
  808. peer,
  809. st::botEmojiStatusUserpic);
  810. name->setAttribute(Qt::WA_TransparentForMouseEvents);
  811. userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
  812. if (right) {
  813. right->setParent(result.get());
  814. right->show();
  815. right->setAttribute(Qt::WA_TransparentForMouseEvents);
  816. }
  817. auto rightWidth = right ? right->widthValue() : rpl::single(0);
  818. raw->resize(size, size);
  819. rpl::combine(
  820. raw->sizeValue(),
  821. std::move(rightWidth)
  822. ) | rpl::start_with_next([=](QSize outer, int rwidth) {
  823. const auto full = outer.width();
  824. const auto decorations = size
  825. + padding.left()
  826. + padding.right()
  827. + rwidth;
  828. const auto inner = full - decorations;
  829. const auto use = std::min(inner, name->textMaxWidth());
  830. *width = use + decorations;
  831. const auto left = (full - *width) / 2;
  832. if (inner > 0) {
  833. userpic->moveToLeft(left, 0, outer.width());
  834. if (right) {
  835. right->moveToLeft(
  836. left + *width - padding.right() - rwidth,
  837. padding.top(),
  838. outer.width());
  839. }
  840. name->resizeToWidth(use);
  841. name->moveToLeft(
  842. left + size + padding.left(),
  843. padding.top(),
  844. outer.width());
  845. }
  846. }, raw->lifetime());
  847. raw->paintRequest() | rpl::start_with_next([=] {
  848. auto p = QPainter(raw);
  849. const auto left = (raw->width() - *width) / 2;
  850. const auto skip = size / 2;
  851. p.setClipRect(left + skip, 0, *width - skip, size);
  852. auto hq = PainterHighQualityEnabler(p);
  853. p.setPen(Qt::NoPen);
  854. p.setBrush(bgOverride ? *bgOverride : st::windowBgOver);
  855. p.drawRoundedRect(left, 0, *width, size, skip, skip);
  856. }, raw->lifetime());
  857. return result;
  858. }
  859. void ConfirmUpdate(
  860. std::shared_ptr<Ui::Show> show,
  861. not_null<UserData*> bot,
  862. const StarRefProgram &program,
  863. bool exists,
  864. Fn<void(Fn<void(bool)>)> update) {
  865. show->show(Box([=](not_null<Ui::GenericBox*> box) {
  866. const auto sent = std::make_shared<bool>();
  867. Ui::ConfirmBox(box, {
  868. .text = (exists
  869. ? tr::lng_star_ref_warning_change
  870. : tr::lng_star_ref_warning_text)(Ui::Text::RichLangValue),
  871. .confirmed = [=](Fn<void()> close) {
  872. if (*sent) {
  873. return;
  874. }
  875. *sent = true;
  876. update([=](bool success) {
  877. *sent = false;
  878. if (success) {
  879. close();
  880. }
  881. });
  882. },
  883. .confirmText = (exists
  884. ? tr::lng_star_ref_warning_update
  885. : tr::lng_star_ref_warning_start)(),
  886. .title = tr::lng_star_ref_warning_title(),
  887. });
  888. auto table = box->addRow(
  889. object_ptr<Ui::TableLayout>(
  890. box,
  891. st::giveawayGiftCodeTable),
  892. st::giveawayGiftCodeTableMargin);
  893. const auto addRow = [&](
  894. rpl::producer<QString> label,
  895. const QString &value) {
  896. table->addRow(
  897. object_ptr<Ui::FlatLabel>(
  898. table,
  899. std::move(label),
  900. table->st().defaultLabel),
  901. object_ptr<Ui::FlatLabel>(
  902. table,
  903. value,
  904. table->st().defaultValue,
  905. st::defaultPopupMenu),
  906. st::giveawayGiftCodeLabelMargin,
  907. st::giveawayGiftCodeValueMargin);
  908. };
  909. addRow(
  910. tr::lng_star_ref_commission_title(),
  911. FormatCommission(program.commission));
  912. addRow(
  913. tr::lng_star_ref_duration_title(),
  914. FormatProgramDuration(program.durationMonths));
  915. }));
  916. }
  917. void UpdateProgram(
  918. std::shared_ptr<Ui::Show> show,
  919. not_null<UserData*> bot,
  920. const StarRefProgram &program,
  921. Fn<void(bool)> done) {
  922. using Flag = MTPbots_UpdateStarRefProgram::Flag;
  923. bot->session().api().request(MTPbots_UpdateStarRefProgram(
  924. MTP_flags((program.commission > 0 && program.durationMonths > 0)
  925. ? Flag::f_duration_months
  926. : Flag()),
  927. bot->inputUser,
  928. MTP_int(program.commission),
  929. MTP_int(program.durationMonths)
  930. )).done([=](const MTPStarRefProgram &result) {
  931. bot->setStarRefProgram(Data::ParseStarRefProgram(&result));
  932. done(true);
  933. }).fail([=](const MTP::Error &error) {
  934. show->showToast(u"Failed: "_q + error.type());
  935. done(false);
  936. }).send();
  937. }
  938. void FinishProgram(
  939. std::shared_ptr<Ui::Show> show,
  940. not_null<UserData*> bot,
  941. Fn<void(bool)> done) {
  942. UpdateProgram(std::move(show), bot, {}, std::move(done));
  943. }
  944. ConnectedBots Parse(
  945. not_null<Main::Session*> session,
  946. const MTPpayments_ConnectedStarRefBots &bots) {
  947. const auto &data = bots.data();
  948. session->data().processUsers(data.vusers());
  949. const auto &list = data.vconnected_bots().v;
  950. auto result = ConnectedBots();
  951. for (const auto &bot : list) {
  952. const auto &data = bot.data();
  953. const auto botId = UserId(data.vbot_id());
  954. const auto link = qs(data.vurl());
  955. const auto date = data.vdate().v;
  956. const auto commission = data.vcommission_permille().v;
  957. const auto durationMonths
  958. = data.vduration_months().value_or_empty();
  959. const auto users = int(data.vparticipants().v);
  960. const auto revoked = data.is_revoked();
  961. result.push_back({
  962. .bot = session->data().user(botId),
  963. .state = {
  964. .program = {
  965. .commission = ushort(commission),
  966. .durationMonths = uchar(durationMonths),
  967. },
  968. .link = link,
  969. .date = date,
  970. .users = users,
  971. .revoked = revoked,
  972. },
  973. });
  974. }
  975. return result;
  976. }
  977. } // namespace Info::BotStarRef