/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/peer_gifts/info_peer_gifts_widget.h" #include "api/api_premium.h" #include "apiwrap.h" #include "data/data_channel.h" #include "data/data_credits.h" #include "data/data_session.h" #include "data/data_user.h" #include "info/peer_gifts/info_peer_gifts_common.h" #include "info/info_controller.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/wrap/slide_wrap.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "mtproto/sender.h" #include "window/window_session_controller.h" #include "settings/settings_credits_graphics.h" #include "styles/style_info.h" #include "styles/style_layers.h" // boxRadius #include "styles/style_media_player.h" // mediaPlayerMenuCheck #include "styles/style_menu_icons.h" #include "styles/style_credits.h" // giftBoxPadding namespace Info::PeerGifts { namespace { constexpr auto kPreloadPages = 2; constexpr auto kPerPage = 50; [[nodiscard]] GiftDescriptor DescriptorForGift( not_null to, const Data::SavedStarGift &gift) { return GiftTypeStars{ .info = gift.info, .from = ((gift.anonymous || !gift.fromId) ? nullptr : to->owner().peer(gift.fromId).get()), .date = gift.date, .userpic = !gift.info.unique, .pinned = gift.pinned, .hidden = gift.hidden, .mine = to->isSelf(), }; } } // namespace class InnerWidget final : public Ui::BoxContentDivider { public: InnerWidget( QWidget *parent, not_null controller, not_null peer, rpl::producer filter); [[nodiscard]] not_null peer() const { return _peer; } [[nodiscard]] rpl::producer notifyEnabled() const { return _notifyEnabled.events(); } [[nodiscard]] rpl::producer<> scrollToTop() const { return _scrollToTop.events(); } void saveState(not_null memento); void restoreState(not_null memento); private: struct Entry { Data::SavedStarGift gift; GiftDescriptor descriptor; }; struct View { std::unique_ptr button; Data::SavedStarGiftId manageId; uint64 giftId = 0; int index = 0; }; void visibleTopBottomUpdated( int visibleTop, int visibleBottom) override; void paintEvent(QPaintEvent *e) override; void subscribeToUpdates(); void loadMore(); void refreshButtons(); void validateButtons(); void showGift(int index); void showMenuFor(not_null button, QPoint point); void refreshAbout(); void markPinned(std::vector::iterator i); void markUnpinned(std::vector::iterator i); int resizeGetHeight(int width) override; [[nodiscard]] auto pinnedSavedGifts() -> Fn()>; const not_null _window; rpl::variable _filter; Delegate _delegate; not_null _controller; std::unique_ptr _about; const not_null _peer; std::vector _entries; int _totalCount = 0; rpl::event_stream<> _scrollToTop; MTP::Sender _api; mtpRequestId _loadMoreRequestId = 0; QString _offset; bool _allLoaded = false; bool _reloading = false; rpl::event_stream _notifyEnabled; std::vector _views; int _viewsForWidth = 0; int _viewsFromRow = 0; int _viewsTillRow = 0; QSize _singleMin; QSize _single; int _perRow = 0; int _visibleFrom = 0; int _visibleTill = 0; base::unique_qptr _menu; }; InnerWidget::InnerWidget( QWidget *parent, not_null controller, not_null peer, rpl::producer filter) : BoxContentDivider(parent) , _window(controller->parentController()) , _filter(std::move(filter)) , _delegate(&_window->session(), GiftButtonMode::Minimal) , _controller(controller) , _peer(peer) , _totalCount(_peer->peerGiftsCount()) , _api(&_peer->session().mtp()) { _singleMin = _delegate.buttonSize(); if (peer->canManageGifts()) { subscribeToUpdates(); } _filter.value() | rpl::start_with_next([=] { _reloading = true; _api.request(base::take(_loadMoreRequestId)).cancel(); _allLoaded = false; refreshAbout(); loadMore(); }, lifetime()); } void InnerWidget::subscribeToUpdates() { _peer->owner().giftUpdates( ) | rpl::start_with_next([=](const Data::GiftUpdate &update) { const auto savedId = [](const Entry &entry) { return entry.gift.manageId; }; const auto i = ranges::find(_entries, update.id, savedId); if (i == end(_entries)) { return; } const auto index = int(i - begin(_entries)); using Action = Data::GiftUpdate::Action; if (update.action == Action::Convert || update.action == Action::Transfer || update.action == Action::Delete) { _entries.erase(i); if (_totalCount > 0) { --_totalCount; } for (auto &view : _views) { if (view.index >= index) { --view.index; } } } else if (update.action == Action::Save || update.action == Action::Unsave) { i->gift.hidden = (update.action == Action::Unsave); const auto unpin = i->gift.hidden && i->gift.pinned; v::match(i->descriptor, [](GiftTypePremium &) { }, [&](GiftTypeStars &data) { data.hidden = i->gift.hidden; }); for (auto &view : _views) { if (view.index == index) { view.index = -1; view.manageId = {}; } } if (unpin) { markUnpinned(i); } } else if (update.action == Action::Pin || update.action == Action::Unpin) { if (update.action == Action::Pin) { markPinned(i); } else { markUnpinned(i); } } else { return; } refreshButtons(); if (update.action == Action::Pin) { _scrollToTop.fire({}); } }, lifetime()); } void InnerWidget::markPinned(std::vector::iterator i) { const auto index = int(i - begin(_entries)); i->gift.pinned = true; v::match(i->descriptor, [](const GiftTypePremium &) { }, [&](GiftTypeStars &data) { data.pinned = true; }); if (index) { std::rotate(begin(_entries), i, i + 1); } auto unpin = end(_entries); const auto session = &_window->session(); const auto limit = session->appConfig().pinnedGiftsLimit(); if (limit < _entries.size()) { const auto j = begin(_entries) + limit; if (j->gift.pinned) { unpin = j; } } for (auto &view : _views) { if (view.index <= index) { view.index = -1; view.manageId = {}; } } if (unpin != end(_entries)) { markUnpinned(unpin); } } void InnerWidget::markUnpinned(std::vector::iterator i) { const auto index = int(i - begin(_entries)); i->gift.pinned = false; v::match(i->descriptor, [](const GiftTypePremium &) { }, [&](GiftTypeStars &data) { data.pinned = false; }); auto after = index + 1; for (auto j = i + 1; j != end(_entries); ++j) { if (!j->gift.pinned && j->gift.date <= i->gift.date) { break; } ++after; } if (after == _entries.size() && !_allLoaded) { // We don't know if the correct position is exactly in the end // of the loaded part or later, so we hide it for now, let it // be loaded later while scrolling. _entries.erase(i); } else if (after > index + 1) { std::rotate(i, i + 1, begin(_entries) + after); } for (auto &view : _views) { if (view.index >= index) { view.index = -1; view.manageId = {}; } } } void InnerWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { const auto page = (visibleBottom - visibleTop); if (visibleBottom + page * kPreloadPages >= height()) { loadMore(); } _visibleFrom = visibleTop; _visibleTill = visibleBottom; validateButtons(); } void InnerWidget::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto aboutSize = _about ? _about->size().grownBy(st::giftListAboutMargin) : QSize(); const auto skips = QMargins(0, 0, 0, aboutSize.height()); p.fillRect(rect().marginsRemoved(skips), st::boxDividerBg->c); paintTop(p); if (const auto bottom = skips.bottom()) { paintBottom(p, bottom); } } void InnerWidget::loadMore() { if (_allLoaded || _loadMoreRequestId) { return; } using Flag = MTPpayments_GetSavedStarGifts::Flag; const auto filter = _filter.current(); _loadMoreRequestId = _api.request(MTPpayments_GetSavedStarGifts( MTP_flags((filter.sortByValue ? Flag::f_sort_by_value : Flag()) | (filter.skipLimited ? Flag::f_exclude_limited : Flag()) | (filter.skipUnlimited ? Flag::f_exclude_unlimited : Flag()) | (filter.skipUnique ? Flag::f_exclude_unique : Flag()) | (filter.skipSaved ? Flag::f_exclude_saved : Flag()) | (filter.skipUnsaved ? Flag::f_exclude_unsaved : Flag())), _peer->input, MTP_string(_reloading ? QString() : _offset), MTP_int(kPerPage) )).done([=](const MTPpayments_SavedStarGifts &result) { _loadMoreRequestId = 0; const auto &data = result.data(); if (const auto enabled = data.vchat_notifications_enabled()) { _notifyEnabled.fire(mtpIsTrue(*enabled)); } if (const auto next = data.vnext_offset()) { _offset = qs(*next); } else { _allLoaded = true; } _totalCount = data.vcount().v; const auto owner = &_peer->owner(); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); if (base::take(_reloading)) { _entries.clear(); } _entries.reserve(_entries.size() + data.vgifts().v.size()); for (const auto &gift : data.vgifts().v) { if (auto parsed = Api::FromTL(_peer, gift)) { auto descriptor = DescriptorForGift(_peer, *parsed); _entries.push_back({ .gift = std::move(*parsed), .descriptor = std::move(descriptor), }); } } refreshButtons(); refreshAbout(); }).fail([=] { _loadMoreRequestId = 0; _allLoaded = true; }).send(); } void InnerWidget::refreshButtons() { _viewsForWidth = 0; _viewsFromRow = 0; _viewsTillRow = 0; resizeToWidth(width()); validateButtons(); } void InnerWidget::validateButtons() { if (!_perRow) { return; } const auto padding = st::giftBoxPadding; const auto vskip = padding.bottom(); const auto row = _single.height() + st::giftBoxGiftSkip.y(); const auto fromRow = std::max(_visibleFrom - vskip, 0) / row; const auto tillRow = (_visibleTill - vskip + row - 1) / row; Assert(tillRow >= fromRow); if (_viewsFromRow == fromRow && _viewsTillRow == tillRow && _viewsForWidth == width()) { return; } _viewsFromRow = fromRow; _viewsTillRow = tillRow; _viewsForWidth = width(); const auto available = _viewsForWidth - padding.left() - padding.right(); const auto skipw = st::giftBoxGiftSkip.x(); const auto fullw = _perRow * (_single.width() + skipw) - skipw; const auto left = padding.left() + (available - fullw) / 2; const auto oneh = _single.height() + st::giftBoxGiftSkip.y(); const auto mode = GiftButton::Mode::Minimal; auto x = left; auto y = vskip + fromRow * oneh; auto views = std::vector(); views.reserve((tillRow - fromRow) * _perRow); const auto idUsed = [&](uint64 giftId, int column, int row) { for (auto j = row; j != tillRow; ++j) { for (auto i = column; i != _perRow; ++i) { const auto index = j * _perRow + i; if (index >= _entries.size()) { return false; } else if (_entries[index].gift.info.id == giftId) { return true; } } column = 0; } return false; }; const auto add = [&](int column, int row) { const auto index = row * _perRow + column; if (index >= _entries.size()) { return false; } const auto giftId = _entries[index].gift.info.id; const auto manageId = _entries[index].gift.manageId; const auto &descriptor = _entries[index].descriptor; const auto already = ranges::find(_views, giftId, &View::giftId); if (already != end(_views)) { views.push_back(base::take(*already)); } else { const auto unused = ranges::find_if(_views, [&](const View &v) { return v.button && !idUsed(v.giftId, column, row); }); if (unused != end(_views)) { views.push_back(base::take(*unused)); } else { auto button = std::make_unique(this, &_delegate); const auto raw = button.get(); raw->contextMenuRequests( ) | rpl::start_with_next([=](QPoint point) { showMenuFor(raw, point); }, raw->lifetime()); raw->show(); views.push_back({ .button = std::move(button) }); } } auto &view = views.back(); const auto callback = [=] { showGift(index); }; view.index = index; view.manageId = manageId; view.giftId = giftId; view.button->setDescriptor(descriptor, mode); view.button->setClickedCallback(callback); return true; }; for (auto j = fromRow; j != tillRow; ++j) { for (auto i = 0; i != _perRow; ++i) { if (!add(i, j)) { break; } views.back().button->setGeometry( QRect(QPoint(x, y), _single), _delegate.buttonExtend()); x += _single.width() + skipw; } x = left; y += oneh; } std::swap(_views, views); } auto InnerWidget::pinnedSavedGifts() -> Fn()> { struct Entry { Data::SavedStarGiftId id; std::shared_ptr unique; }; auto entries = std::vector(); for (const auto &entry : _entries) { if (entry.gift.pinned) { Assert(entry.gift.info.unique != nullptr); entries.push_back({ entry.gift.manageId, entry.gift.info.unique, }); } else { break; } } return [entries] { auto result = std::vector(); result.reserve(entries.size()); for (const auto &entry : entries) { const auto &id = entry.id; result.push_back({ .bareMsgId = uint64(id.userMessageId().bare), .bareEntryOwnerId = id.chat() ? id.chat()->id.value : 0, .giftChannelSavedId = id.chatSavedId(), .uniqueGift = entry.unique, .stargift = true, }); } return result; }; } void InnerWidget::showMenuFor(not_null button, QPoint point) { if (_menu) { return; } const auto index = [&] { for (const auto &view : _views) { if (view.button.get() == button) { return view.index; } } return -1; }(); if (index < 0) { return; } auto entry = ::Settings::SavedStarGiftEntry( _peer, _entries[index].gift); entry.pinnedSavedGifts = pinnedSavedGifts(); _menu = base::make_unique_q(this, st::popupMenuWithIcons); ::Settings::FillSavedStarGiftMenu( _controller->uiShow(), _menu.get(), entry, ::Settings::SavedStarGiftMenuType::List); if (_menu->empty()) { return; } _menu->popup(point); } void InnerWidget::showGift(int index) { Expects(index >= 0 && index < _entries.size()); _window->show(Box( ::Settings::SavedStarGiftBox, _window, _peer, _entries[index].gift, pinnedSavedGifts())); } void InnerWidget::refreshAbout() { if (!_peer->isSelf() && _peer->canManageGifts() && !_entries.empty()) { if (_about) { _about = nullptr; resizeToWidth(width()); } } else if (!_about) { _about = std::make_unique( this, (_peer->isSelf() ? tr::lng_peer_gifts_about_mine(Ui::Text::RichLangValue) : tr::lng_peer_gifts_about( lt_user, rpl::single(Ui::Text::Bold(_peer->shortName())), Ui::Text::RichLangValue)), st::giftListAbout); _about->show(); resizeToWidth(width()); } } int InnerWidget::resizeGetHeight(int width) { const auto count = int(_entries.size()); const auto padding = st::giftBoxPadding; const auto available = width - padding.left() - padding.right(); const auto skipw = st::giftBoxGiftSkip.x(); _perRow = std::min( (available + skipw) / (_singleMin.width() + skipw), std::max(count, 1)); if (!_perRow) { return 0; } const auto singlew = std::min( ((available + skipw) / _perRow) - skipw, 2 * _singleMin.width()); Assert(singlew >= _singleMin.width()); const auto singleh = _singleMin.height(); _single = QSize(singlew, singleh); const auto rows = (count + _perRow - 1) / _perRow; const auto skiph = st::giftBoxGiftSkip.y(); auto result = rows ? (padding.bottom() * 2 + rows * (singleh + skiph) - skiph) : 0; if (const auto about = _about.get()) { const auto margin = st::giftListAboutMargin; about->resizeToWidth(width - margin.left() - margin.right()); about->moveToLeft(margin.left(), result + margin.top()); result += margin.top() + about->height() + margin.bottom(); } return result; } void InnerWidget::saveState(not_null memento) { auto state = std::make_unique(); memento->setListState(std::move(state)); } void InnerWidget::restoreState(not_null memento) { if (const auto state = memento->listState()) { } } Memento::Memento(not_null peer) : ContentMemento(peer, nullptr, PeerId()) { } Section Memento::section() const { return Section(Section::Type::PeerGifts); } object_ptr Memento::createWidget( QWidget *parent, not_null controller, const QRect &geometry) { auto result = object_ptr(parent, controller, peer()); result->setInternalState(geometry, this); return result; } void Memento::setListState(std::unique_ptr state) { _listState = std::move(state); } std::unique_ptr Memento::listState() { return std::move(_listState); } Memento::~Memento() = default; Widget::Widget( QWidget *parent, not_null controller, not_null peer) : ContentWidget(parent, controller) { _inner = setInnerWidget( object_ptr(this, controller, peer, _filter.value())); _inner->notifyEnabled( ) | rpl::take(1) | rpl::start_with_next([=](bool enabled) { setupNotifyCheckbox(enabled); }, _inner->lifetime()); _inner->scrollToTop() | rpl::start_with_next([=] { scrollTo({ 0, 0 }); }, _inner->lifetime()); } void Widget::showFinished() { _shown = true; if (const auto bottom = _pinnedToBottom.data()) { bottom->toggle(true, anim::type::normal); } } void Widget::setupNotifyCheckbox(bool enabled) { _pinnedToBottom = Ui::CreateChild>( this, object_ptr(this)); const auto wrap = _pinnedToBottom.data(); wrap->toggle(false, anim::type::instant); const auto bottom = wrap->entity(); bottom->show(); const auto notify = Ui::CreateChild( bottom, tr::lng_peer_gifts_notify(), enabled); notify->show(); notify->checkedChanges() | rpl::start_with_next([=](bool checked) { const auto api = &controller()->session().api(); const auto show = controller()->uiShow(); using Flag = MTPpayments_ToggleChatStarGiftNotifications::Flag; api->request(MTPpayments_ToggleChatStarGiftNotifications( MTP_flags(checked ? Flag::f_enabled : Flag()), _inner->peer()->input )).send(); if (checked) { show->showToast(tr::lng_peer_gifts_notify_enabled(tr::now)); } }, notify->lifetime()); const auto &checkSt = st::defaultCheckbox; const auto checkTop = st::boxRadius + checkSt.margin.top(); bottom->widthValue() | rpl::start_with_next([=](int width) { const auto normal = notify->naturalWidth() - checkSt.margin.left() - checkSt.margin.right(); notify->resizeToWidth(normal); const auto checkLeft = (width - normal) / 2; notify->moveToLeft(checkLeft, checkTop); }, notify->lifetime()); notify->heightValue() | rpl::start_with_next([=](int height) { bottom->resize(bottom->width(), st::boxRadius + height); }, notify->lifetime()); const auto processHeight = [=] { setScrollBottomSkip(wrap->height()); wrap->moveToLeft(wrap->x(), height() - wrap->height()); }; _inner->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { wrap->resizeToWidth(s.width()); crl::on_main(wrap, processHeight); }, wrap->lifetime()); rpl::combine( wrap->heightValue(), heightValue() ) | rpl::start_with_next(processHeight, wrap->lifetime()); if (_shown) { wrap->toggle(true, anim::type::normal); } _hasPinnedToBottom = true; } void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { const auto filter = _filter.current(); const auto change = [=](Fn update) { auto now = _filter.current(); update(now); _filter = now; }; if (filter.sortByValue) { addAction(tr::lng_peer_gifts_filter_by_date(tr::now), [=] { change([](Filter &filter) { filter.sortByValue = false; }); }, &st::menuIconSchedule); } else { addAction(tr::lng_peer_gifts_filter_by_value(tr::now), [=] { change([](Filter &filter) { filter.sortByValue = true; }); }, &st::menuIconEarn); } addAction({ .isSeparator = true }); addAction(tr::lng_peer_gifts_filter_unlimited(tr::now), [=] { change([](Filter &filter) { filter.skipUnlimited = !filter.skipUnlimited; if (filter.skipUnlimited && filter.skipLimited && filter.skipUnique) { filter.skipLimited = false; } }); }, filter.skipUnlimited ? nullptr : &st::mediaPlayerMenuCheck); addAction(tr::lng_peer_gifts_filter_limited(tr::now), [=] { change([](Filter &filter) { filter.skipLimited = !filter.skipLimited; if (filter.skipUnlimited && filter.skipLimited && filter.skipUnique) { filter.skipUnlimited = false; } }); }, filter.skipLimited ? nullptr : &st::mediaPlayerMenuCheck); addAction(tr::lng_peer_gifts_filter_unique(tr::now), [=] { change([](Filter &filter) { filter.skipUnique = !filter.skipUnique; if (filter.skipUnlimited && filter.skipLimited && filter.skipUnique) { filter.skipUnlimited = false; } }); }, filter.skipUnique ? nullptr : &st::mediaPlayerMenuCheck); if (_inner->peer()->canManageGifts()) { addAction({ .isSeparator = true }); addAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] { change([](Filter &filter) { filter.skipSaved = !filter.skipSaved; if (filter.skipSaved && filter.skipUnsaved) { filter.skipUnsaved = false; } }); }, filter.skipSaved ? nullptr : &st::mediaPlayerMenuCheck); addAction(tr::lng_peer_gifts_filter_unsaved(tr::now), [=] { change([](Filter &filter) { filter.skipUnsaved = !filter.skipUnsaved; if (filter.skipSaved && filter.skipUnsaved) { filter.skipSaved = false; } }); }, filter.skipUnsaved ? nullptr : &st::mediaPlayerMenuCheck); } } rpl::producer Widget::title() { return tr::lng_peer_gifts_title(); } rpl::producer Widget::desiredBottomShadowVisibility() { return _hasPinnedToBottom.value(); } not_null Widget::peer() const { return _inner->peer(); } bool Widget::showInternal(not_null memento) { if (!controller()->validateMementoPeer(memento)) { return false; } if (auto similarMemento = dynamic_cast(memento.get())) { if (similarMemento->peer() == peer()) { restoreState(similarMemento); return true; } } return false; } void Widget::setInternalState( const QRect &geometry, not_null memento) { setGeometry(geometry); Ui::SendPendingMoveResizeEvents(this); restoreState(memento); } std::shared_ptr Widget::doCreateMemento() { auto result = std::make_shared(peer()); saveState(result.get()); return result; } void Widget::saveState(not_null memento) { memento->setScrollTop(scrollTopSave()); _inner->saveState(memento); } void Widget::restoreState(not_null memento) { _inner->restoreState(memento); scrollTopRestore(memento->scrollTop()); } } // namespace Info::PeerGifts