replace_boost_box.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  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/replace_boost_box.h"
  8. #include "api/api_peer_colors.h"
  9. #include "apiwrap.h"
  10. #include "base/event_filter.h"
  11. #include "base/unixtime.h"
  12. #include "data/data_premium_limits.h"
  13. #include "boxes/peer_list_box.h"
  14. #include "data/data_channel.h"
  15. #include "data/data_cloud_themes.h"
  16. #include "data/data_session.h"
  17. #include "lang/lang_keys.h"
  18. #include "main/main_app_config.h"
  19. #include "main/main_session.h"
  20. #include "main/session/session_show.h"
  21. #include "ui/boxes/boost_box.h"
  22. #include "ui/boxes/confirm_box.h"
  23. #include "ui/chat/chat_style.h"
  24. #include "ui/controls/userpic_button.h"
  25. #include "ui/effects/premium_graphics.h"
  26. #include "ui/layers/generic_box.h"
  27. #include "ui/text/text_utilities.h"
  28. #include "ui/toast/toast.h"
  29. #include "ui/widgets/labels.h"
  30. #include "ui/wrap/padding_wrap.h"
  31. #include "ui/wrap/vertical_layout.h"
  32. #include "ui/empty_userpic.h"
  33. #include "ui/painter.h"
  34. #include "styles/style_boxes.h"
  35. #include "styles/style_premium.h"
  36. namespace {
  37. constexpr auto kWaitingOpacity = 0.5;
  38. class Row final : public PeerListRow {
  39. public:
  40. Row(
  41. not_null<Main::Session*> session,
  42. TakenBoostSlot slot,
  43. TimeId unixtimeNow,
  44. crl::time preciseNow);
  45. void updateStatus(TimeId unixtimeNow, crl::time preciseNow);
  46. [[nodiscard]] TakenBoostSlot data() const {
  47. return _data;
  48. }
  49. [[nodiscard]] bool waiting() const {
  50. return _waiting;
  51. }
  52. QString generateName() override;
  53. QString generateShortName() override;
  54. PaintRoundImageCallback generatePaintUserpicCallback(
  55. bool forceRound) override;
  56. float64 opacity() override;
  57. private:
  58. [[nodiscard]]PaintRoundImageCallback peerPaintUserpicCallback();
  59. TakenBoostSlot _data;
  60. PeerData *_peer = nullptr;
  61. std::shared_ptr<Ui::EmptyUserpic> _empty;
  62. Ui::PeerUserpicView _userpic;
  63. crl::time _startPreciseTime = 0;
  64. TimeId _startUnixtime = 0;
  65. bool _waiting = false;
  66. };
  67. class Controller final : public PeerListController {
  68. public:
  69. Controller(not_null<ChannelData*> to, std::vector<TakenBoostSlot> from);
  70. [[nodiscard]] rpl::producer<std::vector<int>> selectedValue() const {
  71. return _selected.value();
  72. }
  73. Main::Session &session() const override;
  74. void prepare() override;
  75. void rowClicked(not_null<PeerListRow*> row) override;
  76. bool trackSelectedList() override {
  77. return false;
  78. }
  79. private:
  80. void updateWaitingState();
  81. not_null<ChannelData*> _to;
  82. std::vector<TakenBoostSlot> _from;
  83. rpl::variable<std::vector<int>> _selected;
  84. rpl::variable<std::vector<not_null<PeerData*>>> _selectedPeers;
  85. base::Timer _waitingTimer;
  86. bool _hasWaitingRows = false;
  87. };
  88. Row::Row(
  89. not_null<Main::Session*> session,
  90. TakenBoostSlot slot,
  91. TimeId unixtimeNow,
  92. crl::time preciseNow)
  93. : PeerListRow(PeerListRowId(slot.id))
  94. , _data(slot)
  95. , _peer(session->data().peerLoaded(_data.peerId))
  96. , _startPreciseTime(preciseNow)
  97. , _startUnixtime(unixtimeNow) {
  98. updateStatus(unixtimeNow, preciseNow);
  99. }
  100. void Row::updateStatus(TimeId unixtimeNow, crl::time preciseNow) {
  101. _waiting = (_data.cooldown > unixtimeNow);
  102. if (_waiting) {
  103. const auto initial = crl::time(_data.cooldown - _startUnixtime);
  104. const auto elapsed = (preciseNow + 500 - _startPreciseTime) / 1000;
  105. const auto seconds = initial
  106. - std::clamp(elapsed, crl::time(), initial);
  107. const auto hours = seconds / 3600;
  108. const auto minutes = seconds / 60;
  109. const auto duration = (hours > 0)
  110. ? u"%1:%2:%3"_q.arg(
  111. hours
  112. ).arg(minutes % 60, 2, 10, QChar('0')
  113. ).arg(seconds % 60, 2, 10, QChar('0'))
  114. : u"%1:%2"_q.arg(
  115. minutes
  116. ).arg(seconds % 60, 2, 10, QChar('0'));
  117. setCustomStatus(
  118. tr::lng_boost_available_in(tr::now, lt_duration, duration));
  119. } else {
  120. const auto date = base::unixtime::parse(_data.expires);
  121. setCustomStatus(tr::lng_boosts_list_status(
  122. tr::now,
  123. lt_date,
  124. langDayOfMonth(date.date())));
  125. }
  126. }
  127. QString Row::generateName() {
  128. return _peer ? _peer->name() : u" "_q;
  129. }
  130. QString Row::generateShortName() {
  131. return _peer ? _peer->shortName() : generateName();
  132. }
  133. PaintRoundImageCallback Row::generatePaintUserpicCallback(
  134. bool forceRound) {
  135. if (_peer) {
  136. return (forceRound && _peer->isForum())
  137. ? ForceRoundUserpicCallback(_peer)
  138. : peerPaintUserpicCallback();
  139. } else if (!_empty) {
  140. const auto colorIndex = _data.id % Ui::kColorIndexCount;
  141. _empty = std::make_shared<Ui::EmptyUserpic>(
  142. Ui::EmptyUserpic::UserpicColor(colorIndex),
  143. u" "_q);
  144. }
  145. const auto empty = _empty;
  146. return [=](Painter &p, int x, int y, int outerWidth, int size) {
  147. empty->paintCircle(p, x, y, outerWidth, size);
  148. };
  149. }
  150. float64 Row::opacity() {
  151. return _waiting ? kWaitingOpacity : 1.;
  152. }
  153. PaintRoundImageCallback Row::peerPaintUserpicCallback() {
  154. const auto peer = _peer;
  155. if (!_userpic.cloud && peer->hasUserpic()) {
  156. _userpic = peer->createUserpicView();
  157. }
  158. auto userpic = _userpic;
  159. return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
  160. peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
  161. };
  162. }
  163. Controller::Controller(
  164. not_null<ChannelData*> to,
  165. std::vector<TakenBoostSlot> from)
  166. : _to(to)
  167. , _from(std::move(from))
  168. , _waitingTimer([=] { updateWaitingState(); }) {
  169. }
  170. Main::Session &Controller::session() const {
  171. return _to->session();
  172. }
  173. void Controller::prepare() {
  174. delegate()->peerListSetTitle(tr::lng_boost_reassign_title());
  175. const auto session = &_to->session();
  176. auto above = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
  177. above->add(
  178. CreateUserpicsTransfer(
  179. above.data(),
  180. _selectedPeers.value(),
  181. _to,
  182. UserpicsTransferType::BoostReplace),
  183. st::boxRowPadding + st::boostReplaceUserpicsPadding);
  184. above->add(
  185. object_ptr<Ui::FlatLabel>(
  186. above.data(),
  187. tr::lng_boost_reassign_text(
  188. lt_channel,
  189. rpl::single(Ui::Text::Bold(_to->name())),
  190. lt_gift,
  191. tr::lng_boost_reassign_gift(
  192. lt_count,
  193. rpl::single(1. * BoostsForGift(session)),
  194. Ui::Text::RichLangValue),
  195. Ui::Text::RichLangValue),
  196. st::boostReassignText),
  197. st::boxRowPadding);
  198. delegate()->peerListSetAboveWidget(std::move(above));
  199. const auto now = base::unixtime::now();
  200. const auto precise = crl::now();
  201. ranges::stable_sort(_from, ranges::less(), [&](TakenBoostSlot slot) {
  202. return (slot.cooldown > now) ? slot.cooldown : -slot.cooldown;
  203. });
  204. for (const auto &slot : _from) {
  205. auto row = std::make_unique<Row>(session, slot, now, precise);
  206. if (row->waiting()) {
  207. _hasWaitingRows = true;
  208. }
  209. delegate()->peerListAppendRow(std::move(row));
  210. }
  211. if (_hasWaitingRows) {
  212. _waitingTimer.callEach(1000);
  213. }
  214. delegate()->peerListRefreshRows();
  215. }
  216. void Controller::updateWaitingState() {
  217. _hasWaitingRows = false;
  218. const auto now = base::unixtime::now();
  219. const auto precise = crl::now();
  220. const auto count = delegate()->peerListFullRowsCount();
  221. for (auto i = 0; i != count; ++i) {
  222. const auto bare = delegate()->peerListRowAt(i);
  223. const auto row = static_cast<Row*>(bare.get());
  224. if (row->waiting()) {
  225. row->updateStatus(now, precise);
  226. delegate()->peerListUpdateRow(row);
  227. if (row->waiting()) {
  228. _hasWaitingRows = true;
  229. }
  230. }
  231. }
  232. if (!_hasWaitingRows) {
  233. _waitingTimer.cancel();
  234. }
  235. }
  236. void Controller::rowClicked(not_null<PeerListRow*> row) {
  237. const auto slot = static_cast<Row*>(row.get())->data();
  238. if (slot.cooldown > base::unixtime::now()) {
  239. delegate()->peerListUiShow()->showToast({
  240. .text = tr::lng_boost_available_in_toast(
  241. tr::now,
  242. lt_count,
  243. BoostsForGift(&session()),
  244. Ui::Text::RichLangValue),
  245. .adaptive = true,
  246. });
  247. return;
  248. }
  249. auto now = _selected.current();
  250. const auto id = slot.id;
  251. const auto checked = !row->checked();
  252. delegate()->peerListSetRowChecked(row, checked);
  253. const auto peer = slot.peerId
  254. ? _to->owner().peerLoaded(slot.peerId)
  255. : nullptr;
  256. auto peerRemoved = false;
  257. if (checked) {
  258. now.push_back(id);
  259. } else {
  260. now.erase(ranges::remove(now, id), end(now));
  261. peerRemoved = true;
  262. for (const auto left : now) {
  263. const auto i = ranges::find(_from, left, &TakenBoostSlot::id);
  264. Assert(i != end(_from));
  265. if (i->peerId == slot.peerId) {
  266. peerRemoved = false;
  267. break;
  268. }
  269. }
  270. }
  271. _selected = std::move(now);
  272. if (peer) {
  273. auto selectedPeers = _selectedPeers.current();
  274. const auto i = ranges::find(selectedPeers, not_null(peer));
  275. if (peerRemoved) {
  276. Assert(i != end(selectedPeers));
  277. selectedPeers.erase(i);
  278. _selectedPeers = std::move(selectedPeers);
  279. } else if (i == end(selectedPeers) && checked) {
  280. selectedPeers.insert(begin(selectedPeers), peer);
  281. _selectedPeers = std::move(selectedPeers);
  282. }
  283. }
  284. }
  285. object_ptr<Ui::BoxContent> ReassignBoostFloodBox(int seconds, bool group) {
  286. const auto days = seconds / 86400;
  287. const auto hours = seconds / 3600;
  288. const auto minutes = seconds / 60;
  289. return Ui::MakeInformBox({
  290. .text = (group
  291. ? tr::lng_boost_error_flood_text_group
  292. : tr::lng_boost_error_flood_text)(
  293. lt_left,
  294. rpl::single(Ui::Text::Bold((days > 1)
  295. ? tr::lng_days(tr::now, lt_count, days)
  296. : (hours > 1)
  297. ? tr::lng_hours(tr::now, lt_count, hours)
  298. : (minutes > 1)
  299. ? tr::lng_minutes(tr::now, lt_count, minutes)
  300. : tr::lng_seconds(tr::now, lt_count, seconds))),
  301. Ui::Text::RichLangValue),
  302. .title = tr::lng_boost_error_flood_title(),
  303. });
  304. }
  305. object_ptr<Ui::BoxContent> ReassignBoostSingleBox(
  306. not_null<ChannelData*> to,
  307. TakenBoostSlot from,
  308. Fn<void(std::vector<int> slots, int groups, int channels)> reassign,
  309. Fn<void()> cancel) {
  310. const auto reassigned = std::make_shared<bool>();
  311. const auto slot = from.id;
  312. const auto peer = to->owner().peer(from.peerId);
  313. const auto group = peer->isMegagroup();
  314. const auto confirmed = [=](Fn<void()> close) {
  315. *reassigned = true;
  316. reassign({ slot }, group ? 1 : 0, group ? 0 : 1);
  317. close();
  318. };
  319. auto result = Box([=](not_null<Ui::GenericBox*> box) {
  320. Ui::ConfirmBox(box, {
  321. .text = tr::lng_boost_now_instead(
  322. lt_channel,
  323. rpl::single(Ui::Text::Bold(peer->name())),
  324. lt_other,
  325. rpl::single(Ui::Text::Bold(to->name())),
  326. Ui::Text::WithEntities),
  327. .confirmed = confirmed,
  328. .confirmText = tr::lng_boost_now_replace(),
  329. .labelPadding = st::boxRowPadding,
  330. });
  331. box->verticalLayout()->insert(
  332. 0,
  333. CreateUserpicsTransfer(
  334. box,
  335. rpl::single(std::vector{ peer }),
  336. to,
  337. UserpicsTransferType::BoostReplace),
  338. st::boxRowPadding + st::boostReplaceUserpicsPadding);
  339. });
  340. result->boxClosing() | rpl::filter([=] {
  341. return !*reassigned;
  342. }) | rpl::start_with_next(cancel, result->lifetime());
  343. return result;
  344. }
  345. } // namespace
  346. ForChannelBoostSlots ParseForChannelBoostSlots(
  347. not_null<ChannelData*> channel,
  348. const QVector<MTPMyBoost> &boosts) {
  349. auto result = ForChannelBoostSlots();
  350. const auto now = base::unixtime::now();
  351. for (const auto &my : boosts) {
  352. const auto &data = my.data();
  353. const auto id = data.vslot().v;
  354. const auto cooldown = data.vcooldown_until_date().value_or(0);
  355. const auto peerId = data.vpeer()
  356. ? peerFromMTP(*data.vpeer())
  357. : PeerId();
  358. if (!peerId && cooldown <= now) {
  359. result.free.push_back(id);
  360. } else if (peerId == channel->id) {
  361. result.already.push_back(id);
  362. } else {
  363. result.other.push_back({
  364. .id = id,
  365. .expires = data.vexpires().v,
  366. .peerId = peerId,
  367. .cooldown = cooldown,
  368. });
  369. }
  370. }
  371. return result;
  372. }
  373. Ui::BoostCounters ParseBoostCounters(
  374. const MTPpremium_BoostsStatus &status) {
  375. const auto &data = status.data();
  376. const auto slots = data.vmy_boost_slots();
  377. return {
  378. .level = data.vlevel().v,
  379. .boosts = data.vboosts().v,
  380. .thisLevelBoosts = data.vcurrent_level_boosts().v,
  381. .nextLevelBoosts = data.vnext_level_boosts().value_or_empty(),
  382. .mine = slots ? int(slots->v.size()) : 0,
  383. };
  384. }
  385. Ui::BoostFeatures LookupBoostFeatures(not_null<ChannelData*> channel) {
  386. auto nameColorsByLevel = base::flat_map<int, int>();
  387. auto linkStylesByLevel = base::flat_map<int, int>();
  388. const auto group = channel->isMegagroup();
  389. const auto peerColors = &channel->session().api().peerColors();
  390. const auto &list = group
  391. ? peerColors->requiredLevelsGroup()
  392. : peerColors->requiredLevelsChannel();
  393. const auto indices = peerColors->indicesCurrent();
  394. for (const auto &[index, level] : list) {
  395. if (!Ui::ColorPatternIndex(indices, index, false)) {
  396. ++nameColorsByLevel[level];
  397. }
  398. ++linkStylesByLevel[level];
  399. }
  400. const auto &themes = channel->owner().cloudThemes().chatThemes();
  401. if (themes.empty()) {
  402. channel->owner().cloudThemes().refreshChatThemes();
  403. }
  404. const auto levelLimits = Data::LevelLimits(&channel->session());
  405. return Ui::BoostFeatures{
  406. .nameColorsByLevel = std::move(nameColorsByLevel),
  407. .linkStylesByLevel = std::move(linkStylesByLevel),
  408. .linkLogoLevel = group ? 0 : levelLimits.channelBgIconLevelMin(),
  409. .transcribeLevel = group ? levelLimits.groupTranscribeLevelMin() : 0,
  410. .emojiPackLevel = group ? levelLimits.groupEmojiStickersLevelMin() : 0,
  411. .emojiStatusLevel = group
  412. ? levelLimits.groupEmojiStatusLevelMin()
  413. : levelLimits.channelEmojiStatusLevelMin(),
  414. .wallpaperLevel = group
  415. ? levelLimits.groupWallpaperLevelMin()
  416. : levelLimits.channelWallpaperLevelMin(),
  417. .wallpapersCount = themes.empty() ? 8 : int(themes.size()),
  418. .customWallpaperLevel = group
  419. ? levelLimits.groupCustomWallpaperLevelMin()
  420. : levelLimits.channelCustomWallpaperLevelMin(),
  421. .sponsoredLevel = levelLimits.channelRestrictSponsoredLevelMin(),
  422. };
  423. }
  424. int BoostsForGift(not_null<Main::Session*> session) {
  425. return session->appConfig().get<int>(u"boosts_per_sent_gift"_q, 0);
  426. }
  427. struct Sources {
  428. int groups = 0;
  429. int channels = 0;
  430. };
  431. [[nodiscard]] Sources SourcesCount(
  432. not_null<ChannelData*> to,
  433. const std::vector<TakenBoostSlot> &from,
  434. const std::vector<int> &slots) {
  435. auto groups = base::flat_set<PeerId>();
  436. groups.reserve(slots.size());
  437. auto channels = base::flat_set<PeerId>();
  438. channels.reserve(slots.size());
  439. const auto owner = &to->owner();
  440. for (const auto slot : slots) {
  441. const auto i = ranges::find(from, slot, &TakenBoostSlot::id);
  442. Assert(i != end(from));
  443. const auto id = i->peerId;
  444. if (!groups.contains(id) && !channels.contains(id)) {
  445. (owner->peer(id)->isMegagroup() ? groups : channels).insert(id);
  446. }
  447. }
  448. return {
  449. .groups = int(groups.size()),
  450. .channels = int(channels.size()),
  451. };
  452. }
  453. object_ptr<Ui::BoxContent> ReassignBoostsBox(
  454. not_null<ChannelData*> to,
  455. std::vector<TakenBoostSlot> from,
  456. Fn<void(std::vector<int> slots, int groups, int channels)> reassign,
  457. Fn<void()> cancel) {
  458. Expects(!from.empty());
  459. const auto now = base::unixtime::now();
  460. if (from.size() == 1 && from.front().cooldown > now) {
  461. cancel();
  462. return ReassignBoostFloodBox(
  463. from.front().cooldown - now,
  464. to->owner().peer(from.front().peerId)->isMegagroup());
  465. } else if (from.size() == 1 && from.front().peerId) {
  466. return ReassignBoostSingleBox(to, from.front(), reassign, cancel);
  467. }
  468. const auto reassigned = std::make_shared<bool>();
  469. auto controller = std::make_unique<Controller>(to, from);
  470. const auto raw = controller.get();
  471. auto initBox = [=](not_null<Ui::BoxContent*> box) {
  472. raw->selectedValue(
  473. ) | rpl::start_with_next([=](std::vector<int> slots) {
  474. box->clearButtons();
  475. if (!slots.empty()) {
  476. const auto sources = SourcesCount(to, from, slots);
  477. box->addButton(tr::lng_boost_reassign_button(), [=] {
  478. *reassigned = true;
  479. reassign(slots, sources.groups, sources.channels);
  480. });
  481. }
  482. box->addButton(tr::lng_cancel(), [=] {
  483. box->closeBox();
  484. });
  485. }, box->lifetime());
  486. box->boxClosing() | rpl::filter([=] {
  487. return !*reassigned;
  488. }) | rpl::start_with_next(cancel, box->lifetime());
  489. };
  490. return Box<PeerListBox>(std::move(controller), std::move(initBox));
  491. }
  492. object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
  493. not_null<Ui::RpWidget*> parent,
  494. rpl::producer<std::vector<not_null<PeerData*>>> from,
  495. not_null<PeerData*> to,
  496. UserpicsTransferType type) {
  497. struct State {
  498. std::vector<not_null<PeerData*>> from;
  499. std::vector<std::unique_ptr<Ui::UserpicButton>> buttons;
  500. QImage layer;
  501. rpl::variable<int> count = 0;
  502. bool painting = false;
  503. };
  504. const auto full = st::boostReplaceUserpic.size.height()
  505. + st::boostReplaceIconAdd.y()
  506. + st::lineWidth;
  507. auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
  508. const auto raw = result.data();
  509. const auto &st = st::boostReplaceUserpic;
  510. const auto right = CreateChild<Ui::UserpicButton>(raw, to, st);
  511. const auto overlay = CreateChild<Ui::RpWidget>(raw);
  512. const auto state = raw->lifetime().make_state<State>();
  513. std::move(
  514. from
  515. ) | rpl::start_with_next([=](
  516. const std::vector<not_null<PeerData*>> &list) {
  517. const auto &st = st::boostReplaceUserpic;
  518. auto was = base::take(state->from);
  519. auto buttons = base::take(state->buttons);
  520. state->from.reserve(list.size());
  521. state->buttons.reserve(list.size());
  522. for (const auto &peer : list) {
  523. state->from.push_back(peer);
  524. const auto i = ranges::find(was, peer);
  525. if (i != end(was)) {
  526. const auto index = int(i - begin(was));
  527. Assert(buttons[index] != nullptr);
  528. state->buttons.push_back(std::move(buttons[index]));
  529. } else {
  530. state->buttons.push_back(
  531. std::make_unique<Ui::UserpicButton>(raw, peer, st));
  532. const auto raw = state->buttons.back().get();
  533. base::install_event_filter(raw, [=](not_null<QEvent*> e) {
  534. return (e->type() == QEvent::Paint && !state->painting)
  535. ? base::EventFilterResult::Cancel
  536. : base::EventFilterResult::Continue;
  537. });
  538. }
  539. }
  540. state->count.force_assign(int(list.size()));
  541. overlay->update();
  542. }, raw->lifetime());
  543. rpl::combine(
  544. raw->widthValue(),
  545. state->count.value()
  546. ) | rpl::start_with_next([=](int width, int count) {
  547. const auto skip = st::boostReplaceUserpicsSkip;
  548. const auto left = width - 2 * right->width() - skip;
  549. const auto shift = std::min(
  550. st::boostReplaceUserpicsShift,
  551. (count > 1 ? (left / (count - 1)) : width));
  552. const auto total = right->width()
  553. + (count ? (skip + right->width() + (count - 1) * shift) : 0);
  554. auto x = (width - total) / 2;
  555. for (const auto &single : state->buttons) {
  556. single->moveToLeft(x, 0);
  557. x += shift;
  558. }
  559. if (count) {
  560. x += right->width() - shift + skip;
  561. }
  562. right->moveToLeft(x, 0);
  563. overlay->setGeometry(QRect(0, 0, width, raw->height()));
  564. }, raw->lifetime());
  565. overlay->paintRequest(
  566. ) | rpl::filter([=] {
  567. return !state->buttons.empty();
  568. }) | rpl::start_with_next([=] {
  569. const auto outerw = overlay->width();
  570. const auto ratio = style::DevicePixelRatio();
  571. if (state->layer.size() != QSize(outerw, full) * ratio) {
  572. state->layer = QImage(
  573. QSize(outerw, full) * ratio,
  574. QImage::Format_ARGB32_Premultiplied);
  575. state->layer.setDevicePixelRatio(ratio);
  576. }
  577. state->layer.fill(Qt::transparent);
  578. auto q = QPainter(&state->layer);
  579. auto hq = PainterHighQualityEnabler(q);
  580. const auto stroke = st::boostReplaceIconOutline;
  581. const auto half = stroke / 2.;
  582. auto pen = st::windowBg->p;
  583. pen.setWidthF(stroke * 2.);
  584. state->painting = true;
  585. for (const auto &button : state->buttons) {
  586. q.setPen(pen);
  587. q.setBrush(Qt::NoBrush);
  588. q.drawEllipse(button->geometry());
  589. const auto position = button->pos();
  590. button->render(&q, position, QRegion(), QWidget::DrawChildren);
  591. }
  592. state->painting = false;
  593. const auto boosting = (type == UserpicsTransferType::BoostReplace);
  594. const auto last = state->buttons.back().get();
  595. const auto back = boosting ? last : right;
  596. const auto add = st::boostReplaceIconAdd;
  597. const auto &icon = boosting
  598. ? st::boostReplaceIcon
  599. : st::starrefJoinIcon;
  600. const auto skip = boosting ? st::boostReplaceIconSkip : 0;
  601. const auto w = icon.width() + 2 * skip;
  602. const auto h = icon.height() + 2 * skip;
  603. const auto x = back->x() + back->width() - w + add.x();
  604. const auto y = back->y() + back->height() - h + add.y();
  605. auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y));
  606. brush.setStops(Ui::Premium::ButtonGradientStops());
  607. q.setBrush(brush);
  608. pen.setWidthF(stroke);
  609. q.setPen(pen);
  610. q.drawEllipse(x - half, y - half, w + stroke, h + stroke);
  611. icon.paint(q, x + skip, y + skip, outerw);
  612. const auto size = st::boostReplaceArrow.size();
  613. st::boostReplaceArrow.paint(
  614. q,
  615. (last->x()
  616. + last->width()
  617. + (st::boostReplaceUserpicsSkip - size.width()) / 2),
  618. (last->height() - size.height()) / 2,
  619. outerw);
  620. q.end();
  621. auto p = QPainter(overlay);
  622. p.drawImage(0, 0, state->layer);
  623. }, overlay->lifetime());
  624. return result;
  625. }
  626. object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
  627. not_null<Ui::RpWidget*> parent,
  628. rpl::producer<std::vector<not_null<PeerData*>>> peers,
  629. int limit) {
  630. struct State {
  631. std::vector<not_null<PeerData*>> from;
  632. std::vector<std::unique_ptr<Ui::UserpicButton>> buttons;
  633. QImage layer;
  634. QImage badge;
  635. rpl::variable<int> count = 0;
  636. bool painting = false;
  637. };
  638. const auto full = st::boostReplaceUserpic.size.height()
  639. + st::boostReplaceIconAdd.y()
  640. + st::lineWidth;
  641. auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
  642. const auto raw = result.data();
  643. const auto overlay = CreateChild<Ui::RpWidget>(raw);
  644. const auto state = raw->lifetime().make_state<State>();
  645. std::move(
  646. peers
  647. ) | rpl::start_with_next([=](
  648. const std::vector<not_null<PeerData*>> &list) {
  649. const auto &st = st::boostReplaceUserpic;
  650. auto was = base::take(state->from);
  651. auto buttons = base::take(state->buttons);
  652. state->from.reserve(list.size());
  653. state->buttons.reserve(list.size());
  654. for (const auto &peer : list | ranges::views::take(limit)) {
  655. state->from.push_back(peer);
  656. const auto i = ranges::find(was, peer);
  657. if (i != end(was)) {
  658. const auto index = int(i - begin(was));
  659. Assert(buttons[index] != nullptr);
  660. state->buttons.push_back(std::move(buttons[index]));
  661. } else {
  662. state->buttons.push_back(
  663. std::make_unique<Ui::UserpicButton>(raw, peer, st));
  664. const auto raw = state->buttons.back().get();
  665. base::install_event_filter(raw, [=](not_null<QEvent*> e) {
  666. return (e->type() == QEvent::Paint && !state->painting)
  667. ? base::EventFilterResult::Cancel
  668. : base::EventFilterResult::Continue;
  669. });
  670. }
  671. }
  672. state->count.force_assign(int(list.size()));
  673. overlay->update();
  674. }, raw->lifetime());
  675. rpl::combine(
  676. raw->widthValue(),
  677. state->count.value()
  678. ) | rpl::start_with_next([=](int width, int count) {
  679. const auto &st = st::boostReplaceUserpic;
  680. const auto single = st.size.width();
  681. const auto left = width - single;
  682. const auto used = std::min(count, int(state->buttons.size()));
  683. const auto shift = std::min(
  684. st::boostReplaceUserpicsShift,
  685. (used > 1 ? (left / (used - 1)) : width));
  686. const auto total = used ? (single + (used - 1) * shift) : 0;
  687. auto x = (width - total) / 2;
  688. for (const auto &single : state->buttons) {
  689. single->moveToLeft(x, 0);
  690. x += shift;
  691. }
  692. overlay->setGeometry(QRect(0, 0, width, raw->height()));
  693. }, raw->lifetime());
  694. overlay->paintRequest(
  695. ) | rpl::filter([=] {
  696. return !state->buttons.empty();
  697. }) | rpl::start_with_next([=] {
  698. const auto outerw = overlay->width();
  699. const auto ratio = style::DevicePixelRatio();
  700. if (state->layer.size() != QSize(outerw, full) * ratio) {
  701. state->layer = QImage(
  702. QSize(outerw, full) * ratio,
  703. QImage::Format_ARGB32_Premultiplied);
  704. state->layer.setDevicePixelRatio(ratio);
  705. }
  706. state->layer.fill(Qt::transparent);
  707. auto q = QPainter(&state->layer);
  708. auto hq = PainterHighQualityEnabler(q);
  709. const auto stroke = st::boostReplaceIconOutline;
  710. const auto half = stroke / 2.;
  711. auto pen = st::windowBg->p;
  712. pen.setWidthF(stroke * 2.);
  713. state->painting = true;
  714. for (const auto &button : state->buttons) {
  715. q.setPen(pen);
  716. q.setBrush(Qt::NoBrush);
  717. q.drawEllipse(button->geometry());
  718. const auto position = button->pos();
  719. button->render(&q, position, QRegion(), QWidget::DrawChildren);
  720. }
  721. state->painting = false;
  722. const auto last = state->buttons.back().get();
  723. const auto add = st::boostReplaceIconAdd;
  724. const auto skip = st::boostReplaceIconSkip;
  725. const auto w = st::boostReplaceIcon.width() + 2 * skip;
  726. const auto h = st::boostReplaceIcon.height() + 2 * skip;
  727. const auto x = last->x() + last->width() - w + add.x();
  728. const auto y = last->y() + last->height() - h + add.y();
  729. const auto text = (state->count.current() > limit)
  730. ? ('+' + QString::number(state->count.current() - limit))
  731. : QString();
  732. if (!text.isEmpty()) {
  733. const auto &font = st::semiboldFont;
  734. const auto width = font->width(text);
  735. const auto padded = std::max(w, width + 2 * font->spacew);
  736. const auto rect = QRect(x - (padded - w) / 2, y, padded, h);
  737. auto brush = QLinearGradient(rect.bottomRight(), rect.topLeft());
  738. brush.setStops(Ui::Premium::ButtonGradientStops());
  739. q.setBrush(brush);
  740. pen.setWidthF(stroke);
  741. q.setPen(pen);
  742. const auto rectf = QRectF(rect);
  743. const auto radius = std::min(rect.width(), rect.height()) / 2.;
  744. q.drawRoundedRect(
  745. rectf.marginsAdded(QMarginsF{ half, half, half, half }),
  746. radius,
  747. radius);
  748. q.setFont(font);
  749. q.setPen(st::premiumButtonFg);
  750. q.drawText(rect, Qt::AlignCenter, text);
  751. }
  752. q.end();
  753. auto p = QPainter(overlay);
  754. p.drawImage(0, 0, state->layer);
  755. }, overlay->lifetime());
  756. return result;
  757. }