diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 279a526a8..5b86db84b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -918,6 +918,8 @@ PRIVATE info/bot/earn/info_bot_earn_list.h info/bot/earn/info_bot_earn_widget.cpp info/bot/earn/info_bot_earn_widget.h + info/bot/starref/info_bot_starref_widget.cpp + info/bot/starref/info_bot_starref_widget.h info/channel_statistics/boosts/create_giveaway_box.cpp info/channel_statistics/boosts/create_giveaway_box.h info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp diff --git a/Telegram/Resources/art/affiliate_logo.png b/Telegram/Resources/art/affiliate_logo.png new file mode 100644 index 000000000..d5f9b5087 Binary files /dev/null and b/Telegram/Resources/art/affiliate_logo.png differ diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 7e035ba54..a5459ef75 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -4,6 +4,7 @@ ../../art/bg_thumbnail.png ../../art/bg_initial.jpg ../../art/business_logo.png + ../../art/affiliate_logo.png ../../art/logo_256.png ../../art/logo_256_no_margin.png ../../art/themeimage.jpg diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index dbfcead1a..de763fb3d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "history/admin_log/history_admin_log_section.h" #include "info/bot/earn/info_bot_earn_widget.h" +#include "info/bot/starref/info_bot_starref_widget.h" #include "info/channel_statistics/boosts/info_boosts_widget.h" #include "info/channel_statistics/earn/earn_format.h" #include "info/channel_statistics/earn/earn_icons.h" @@ -358,6 +359,7 @@ private: void fillBotUsernamesButton(); void fillBotCurrencyButton(); void fillBotCreditsButton(); + void fillBotAffiliateProgram(); void fillBotEditIntroButton(); void fillBotEditCommandsButton(); void fillBotEditSettingsButton(); @@ -1181,6 +1183,7 @@ void Controller::fillManageSection() { fillBotUsernamesButton(); fillBotCurrencyButton(); fillBotCreditsButton(); + fillBotAffiliateProgram(); fillBotEditIntroButton(); fillBotEditCommandsButton(); fillBotEditSettingsButton(); @@ -1711,6 +1714,31 @@ void Controller::fillBotCreditsButton() { } +void Controller::fillBotAffiliateProgram() { + Expects(_isBot); + + const auto user = _peer->asUser(); + auto label = user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::StarRefProgram + ) | rpl::map([=] { + const auto commission = user->botInfo + ? user->botInfo->starRefProgram.commission + : 0; + return commission + ? u"%1%"_q.arg(commission) + : tr::lng_manage_peer_bot_star_ref_off(tr::now); + }); + AddButtonWithCount( + _controls.buttonsLayout, + tr::lng_manage_peer_bot_star_ref(), + std::move(label), + [controller = _navigation->parentController(), user] { + controller->showSection(Info::BotStarRef::Make(user)); + }, + { &st::menuIconSharing }); +} + void Controller::fillBotEditIntroButton() { Expects(_isBot); diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index a2c45bb2a..66c0fb9b3 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -92,27 +92,28 @@ struct PeerUpdate { BusinessDetails = (1ULL << 30), Birthday = (1ULL << 31), PersonalChannel = (1ULL << 32), + StarRefProgram = (1ULL << 33), // For chats and channels - InviteLinks = (1ULL << 33), - Members = (1ULL << 34), - Admins = (1ULL << 35), - BannedUsers = (1ULL << 36), - Rights = (1ULL << 37), - PendingRequests = (1ULL << 38), - Reactions = (1ULL << 39), + InviteLinks = (1ULL << 34), + Members = (1ULL << 35), + Admins = (1ULL << 36), + BannedUsers = (1ULL << 37), + Rights = (1ULL << 38), + PendingRequests = (1ULL << 39), + Reactions = (1ULL << 40), // For channels - ChannelAmIn = (1ULL << 40), - StickersSet = (1ULL << 41), - EmojiSet = (1ULL << 42), - ChannelLinkedChat = (1ULL << 43), - ChannelLocation = (1ULL << 44), - Slowmode = (1ULL << 45), - GroupCall = (1ULL << 46), + ChannelAmIn = (1ULL << 41), + StickersSet = (1ULL << 42), + EmojiSet = (1ULL << 43), + ChannelLinkedChat = (1ULL << 44), + ChannelLocation = (1ULL << 45), + Slowmode = (1ULL << 46), + GroupCall = (1ULL << 47), // For iteration - LastUsedBit = (1ULL << 46), + LastUsedBit = (1ULL << 47), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index c8e5466b9..e73f5fecb 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -600,6 +600,20 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { } if (const auto info = user->botInfo.get()) { info->canManageEmojiStatus = update.is_bot_can_manage_emoji_status(); + auto starRefProgram = StarRefProgram(); + if (const auto program = update.vstarref_program()) { + const auto &data = program->data(); + starRefProgram.commission = data.vcommission_permille().v; + starRefProgram.durationMonths + = data.vduration_months().value_or_empty(); + starRefProgram.endDate = data.vend_date().value_or_empty(); + } + if (info->starRefProgram != starRefProgram) { + info->starRefProgram = starRefProgram; + user->session().changes().peerUpdated( + user, + Data::PeerUpdate::Flag::StarRefProgram); + } } if (const auto pinned = update.vpinned_msg_id()) { SetTopPinnedMessageId(user, pinned->v); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 63f03549a..f8d7ba48a 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -19,6 +19,16 @@ struct BotCommand; struct BusinessDetails; } // namespace Data +struct StarRefProgram { + TimeId endDate = 0; + ushort commission = 0; + uint8 durationMonths = 0; + + friend inline constexpr bool operator==( + StarRefProgram, + StarRefProgram) = default; +}; + struct BotInfo { BotInfo(); @@ -44,6 +54,8 @@ struct BotInfo { ChatAdminRights groupAdminRights; ChatAdminRights channelAdminRights; + StarRefProgram starRefProgram; + int version = 0; int descriptionVersion = 0; int activeUsers = 0; diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_widget.cpp b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_widget.cpp new file mode 100644 index 000000000..00e0e7d5c --- /dev/null +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_widget.cpp @@ -0,0 +1,681 @@ +/* +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/bot/starref/info_bot_starref_widget.h" + +#include "apiwrap.h" +#include "base/timer_rpl.h" +#include "base/unixtime.h" +#include "core/click_handler_types.h" +#include "data/data_user.h" +#include "info/profile/info_profile_icon.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/effects/premium_top_bar.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/ui_utility.h" +#include "ui/vertical_list.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" + +namespace Info::BotStarRef { +namespace { + +constexpr auto kDurationForeverValue = 999; +constexpr auto kCommissionDefault = 20; +constexpr auto kDurationDefault = 12; + +} // namespace + +struct State { + not_null user; + StarRefProgram program; + bool exists = false; +}; + +class InnerWidget final : public Ui::RpWidget { +public: + InnerWidget(QWidget *parent, not_null controller); + + [[nodiscard]] not_null peer() const; + [[nodiscard]] not_null state(); + + void showFinished(); + void setInnerFocus(); + + void saveState(not_null memento); + void restoreState(not_null memento); + +private: + void prepare(); + void setupInfo(); + void setupCommission(); + void setupDuration(); + void setupViewExisting(); + void setupEnd(); + + [[nodiscard]] object_ptr infoRow( + rpl::producer title, + rpl::producer text, + not_null icon); + + const not_null _controller; + State _state; + const not_null _container; + +}; + +[[nodiscard]] int ValueForCommission(const State &state) { + return state.program.commission + ? state.program.commission + : kCommissionDefault; +} + +[[nodiscard]] int ValueForDurationMonths(const State &state) { + return state.program.durationMonths + ? state.program.durationMonths + : state.exists + ? kDurationForeverValue + : kDurationDefault; +} + +[[nodiscard]] State StateForPeer(not_null peer) { + const auto user = peer->asUser(); + const auto program = user->botInfo->starRefProgram; + return State{ + .user = user, + .program = program, + .exists = (program.commission > 0), + }; +} + +InnerWidget::InnerWidget(QWidget *parent, not_null controller) +: RpWidget(parent) +, _controller(controller) +, _state(StateForPeer(_controller->key().starrefPeer())) +, _container(Ui::CreateChild(this)) { + prepare(); +} + +not_null InnerWidget::state() { + return &_state; +} + +void InnerWidget::prepare() { + Ui::ResizeFitChild(this, _container); + + setupInfo(); + Ui::AddSkip(_container); + Ui::AddDivider(_container); + setupCommission(); + setupDuration(); + Ui::AddSkip(_container); + setupViewExisting(); + Ui::AddSkip(_container); + Ui::AddDivider(_container); + Ui::AddSkip(_container); + setupEnd(); +} + +void InnerWidget::setupInfo() { + AddSkip(_container, st::defaultVerticalListSkip * 2); + + _container->add(infoRow( + tr::lng_star_ref_share_title(), + tr::lng_star_ref_share_about(), + &st::menuIconPremium)); + + _container->add(infoRow( + tr::lng_star_ref_launch_title(), + tr::lng_star_ref_launch_about(), + &st::menuIconChannel)); + + _container->add(infoRow( + tr::lng_star_ref_let_title(), + tr::lng_star_ref_let_about(), + &st::menuIconStarRefLink)); +} + +void InnerWidget::setupCommission() { + Ui::AddSkip(_container); + Ui::AddSubsectionTitle(_container, tr::lng_star_ref_commission_title()); + + auto values = std::vector(); + for (auto i = 1; i != 91; ++i) { + values.push_back(i); + } + const auto valuesCount = int(values.size()); + + auto sliderWithLabel = ::Settings::MakeSliderWithLabel( + _container, + st::settingsScale, + st::settingsScaleLabel, + st::normalFont->spacew * 2, + st::settingsScaleLabel.style.font->width("90%"), + true); + _container->add( + std::move(sliderWithLabel.widget), + st::settingsBigScalePadding); + const auto slider = sliderWithLabel.slider; + const auto label = sliderWithLabel.label; + + const auto updateLabel = [=](int value) { + const auto labelText = QString::number(value) + '%'; + label->setText(labelText); + }; + const auto commission = ValueForCommission(_state); + const auto setCommission = [=](int value) { + _state.program.commission = value; + updateLabel(value); + }; + updateLabel(commission); + + slider->setPseudoDiscrete( + valuesCount, + [=](int index) { return values[index]; }, + commission, + setCommission, + setCommission); + + Ui::AddSkip(_container); + Ui::AddDividerText(_container, tr::lng_star_ref_commission_about()); +} + +void InnerWidget::setupDuration() { + Ui::AddSkip(_container); + Ui::AddSubsectionTitle(_container, tr::lng_star_ref_duration_title()); + + auto values = std::vector{ 1, 3, 6, 12, 24, 36, 999 }; + const auto valuesCount = int(values.size()); + + auto sliderWithLabel = ::Settings::MakeSliderWithLabel( + _container, + st::settingsScale, + st::settingsScaleLabel, + st::normalFont->spacew * 2, + st::settingsScaleLabel.style.font->width("3y"), + true); + _container->add( + std::move(sliderWithLabel.widget), + st::settingsBigScalePadding); + const auto slider = sliderWithLabel.slider; + const auto label = sliderWithLabel.label; + + const auto updateLabel = [=](int value) { + const auto labelText = (value < 12) + ? (QString::number(value) + 'm') + : (value < 999) + ? (QString::number(value / 12) + 'y') + : u"inf"_q; + label->setText(labelText); + }; + const auto durationMonths = ValueForDurationMonths(_state); + const auto setDurationMonths = [=](int value) { + _state.program.durationMonths = (value == kDurationForeverValue) + ? 0 + : value; + updateLabel(durationMonths); + }; + updateLabel(durationMonths); + + slider->setPseudoDiscrete( + valuesCount, + [=](int index) { return values[index]; }, + durationMonths, + setDurationMonths, + setDurationMonths); + + Ui::AddSkip(_container); + Ui::AddDividerText(_container, tr::lng_star_ref_duration_about()); +} + +void InnerWidget::setupViewExisting() { + const auto &stLabel = st::defaultFlatLabel; + const auto iconSize = st::settingsPremiumIconDouble.size(); + const auto &titlePadding = st::settingsPremiumRowTitlePadding; + const auto &descriptionPadding = st::settingsPremiumRowAboutPadding; + + const auto content = _container; + const auto labelAscent = stLabel.style.font->ascent; + const auto button = Ui::CreateChild( + content.get(), + rpl::single(QString())); + + const auto label = content->add( + object_ptr( + content, + tr::lng_star_ref_existing_title() | Ui::Text::ToBold(), + stLabel), + titlePadding); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto description = content->add( + object_ptr( + content, + tr::lng_star_ref_existing_about(), + st::boxDividerLabel), + descriptionPadding); + description->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto dummy = Ui::CreateChild(content.get()); + dummy->setAttribute(Qt::WA_TransparentForMouseEvents); + + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + dummy->resize(s.width(), iconSize.height()); + }, dummy->lifetime()); + + label->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + dummy->moveToLeft(0, r.y() + (r.height() - labelAscent)); + }, dummy->lifetime()); + + ::Settings::AddButtonIcon(dummy, st::settingsButton, { + .icon = &st::settingsPremiumIconStar, + .backgroundBrush = st::premiumIconBg3, + }); + + rpl::combine( + content->widthValue(), + label->heightValue(), + description->heightValue() + ) | rpl::start_with_next([=, + topPadding = titlePadding, + bottomPadding = descriptionPadding]( + int width, + int topHeight, + int bottomHeight) { + button->resize( + width, + topPadding.top() + + topHeight + + topPadding.bottom() + + bottomPadding.top() + + bottomHeight + + bottomPadding.bottom()); + }, button->lifetime()); + label->topValue( + ) | rpl::start_with_next([=, padding = titlePadding.top()](int top) { + button->moveToLeft(0, top - padding); + }, button->lifetime()); + const auto arrow = Ui::CreateChild( + button, + st::backButton); + arrow->setIconOverride( + &st::settingsPremiumArrow, + &st::settingsPremiumArrowOver); + arrow->setAttribute(Qt::WA_TransparentForMouseEvents); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto &point = st::settingsPremiumArrowShift; + arrow->moveToRight( + -point.x(), + point.y() + (s.height() - arrow->height()) / 2); + }, arrow->lifetime()); + + button->setClickedCallback([=] { + _controller->showToast(u"List or smth.."_q); + }); +} + +void InnerWidget::setupEnd() { + if (!_state.exists) { + return; + } + const auto end = _container->add(object_ptr( + _container, + tr::lng_star_ref_end(), + st::settingsAttentionButton)); + end->setClickedCallback([=] { + using Flag = MTPbots_UpdateStarRefProgram::Flag; + const auto user = _state.user; + const auto weak = Ui::MakeWeak(this); + user->session().api().request(MTPbots_UpdateStarRefProgram( + MTP_flags(0), + user->inputUser, + MTP_int(0), + MTP_int(0) + )).done([=] { + user->botInfo->starRefProgram.commission = 0; + user->botInfo->starRefProgram.durationMonths = 0; + user->updateFullForced(); + if (weak) { + _controller->showToast("Removed!"); + _controller->showBackFromStack(); + } + }).fail(crl::guard(weak, [=] { + _controller->showToast("Remove failed!"); + })).send(); + }); +} + +object_ptr InnerWidget::infoRow( + rpl::producer title, + rpl::producer text, + not_null icon) { + auto result = object_ptr(_container); + const auto raw = result.data(); + + raw->add( + object_ptr( + raw, + std::move(title) | Ui::Text::ToBold(), + st::defaultFlatLabel), + st::settingsPremiumRowTitlePadding); + raw->add( + object_ptr( + raw, + std::move(text), + st::boxDividerLabel), + st::settingsPremiumRowAboutPadding); + object_ptr( + raw, + *icon, + st::starrefInfoIconPosition); + + return result; +} + +not_null InnerWidget::peer() const { + return _controller->key().starrefPeer(); +} + +void InnerWidget::showFinished() { + +} + +void InnerWidget::setInnerFocus() { + setFocus(); +} + +void InnerWidget::saveState(not_null memento) { + +} + +void InnerWidget::restoreState(not_null memento) { + +} + +Memento::Memento(not_null controller) +: ContentMemento(Tag(controller->starrefPeer())) { +} + +Memento::Memento(not_null peer) +: ContentMemento(Tag(peer)) { +} + +Memento::~Memento() = default; + +Section Memento::section() const { + return Section(Section::Type::BotStarRef); +} + +object_ptr Memento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +Widget::Widget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) +, _inner(setInnerWidget(object_ptr(this, controller))) +, _state(_inner->state()) { + _top = setupTop(); + _bottom = setupBottom(); +} + +not_null Widget::peer() const { + return _inner->peer(); +} + +bool Widget::showInternal(not_null memento) { + return (memento->starrefPeer() == peer()); +} + +rpl::producer Widget::title() { + return tr::lng_star_ref_title(); +} + +void Widget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +rpl::producer Widget::desiredShadowVisibility() const { + return rpl::single(true); +} + +void Widget::showFinished() { + _inner->showFinished(); +} + +void Widget::setInnerFocus() { + _inner->setInnerFocus(); +} + +void Widget::enableBackButton() { + _backEnabled = true; +} + +std::shared_ptr Widget::doCreateMemento() { + auto result = std::make_shared(controller()); + 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()); +} + +std::unique_ptr Widget::setupTop() { + auto title = tr::lng_star_ref_title(); + auto about = tr::lng_star_ref_about() | Ui::Text::ToWithEntities(); + + const auto controller = this->controller(); + const auto weak = base::make_weak(controller->parentController()); + const auto clickContextOther = [=] { + return QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + .botStartAutoSubmit = true, + }); + }; + auto result = std::make_unique( + this, + st::userPremiumCover, + Ui::Premium::TopBarDescriptor{ + .clickContextOther = clickContextOther, + .logo = u"affiliate"_q, + .title = std::move(title), + .about = std::move(about), + .light = true, + }); + const auto raw = result.get(); + + controller->wrapValue( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + raw->setRoundEdges(wrap == Info::Wrap::Layer); + }, raw->lifetime()); + + const auto calculateMaximumHeight = [=] { + return st::settingsPremiumTopHeight; + }; + + raw->setMaximumHeight(st::settingsPremiumTopHeight); + raw->setMinimumHeight(st::settingsPremiumTopHeight); + + raw->resize(width(), raw->maximumHeight()); + + setPaintPadding({ 0, st::settingsPremiumTopHeight, 0, 0 }); + + controller->wrapValue( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + const auto isLayer = (wrap == Info::Wrap::Layer); + _back = base::make_unique_q>( + raw, + object_ptr( + raw, + (isLayer + ? st::infoLayerTopBar.back + : st::infoTopBar.back)), + st::infoTopBarScale); + _back->setDuration(0); + _back->toggleOn(isLayer + ? _backEnabled.value() | rpl::type_erased() + : rpl::single(true)); + _back->entity()->addClickHandler([=] { + controller->showBackFromStack(); + }); + _back->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + const auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar; + raw->setTextPosition( + toggled ? st.back.width : st.titlePosition.x(), + st.titlePosition.y()); + }, _back->lifetime()); + + if (!isLayer) { + _close = nullptr; + } else { + _close = base::make_unique_q( + raw, + st::settingsPremiumTopBarClose); + _close->addClickHandler([=] { + controller->parentController()->hideLayer(); + controller->parentController()->hideSpecialLayer(); + }); + raw->widthValue( + ) | rpl::start_with_next([=] { + _close->moveToRight(0, 0); + }, _close->lifetime()); + } + }, raw->lifetime()); + + raw->move(0, 0); + widthValue() | rpl::start_with_next([=](int width) { + raw->resizeToWidth(width); + setScrollTopSkip(raw->height()); + }, raw->lifetime()); + + return result; +} + +std::unique_ptr Widget::setupBottom() { + auto result = std::make_unique(this); + const auto raw = result.get(); + + auto text = base::timer_each(100) | rpl::map([=] { + const auto till = _state->user->botInfo->starRefProgram.endDate; + const auto now = base::unixtime::now(); + const auto left = (till > now) ? (till - now) : 0; + return left + ? tr::lng_star_ref_start_disabled( + tr::now, + lt_time, + QString::number(left)) + : _state->exists + ? tr::lng_star_ref_update(tr::now) + : tr::lng_star_ref_start(tr::now); + }); + const auto save = raw->add( + object_ptr( + raw, + rpl::duplicate(text), + st::defaultActiveButton), + st::starrefButtonMargin); + std::move(text) | rpl::start_with_next([=] { + save->resizeToWidth(raw->width() + - st::starrefButtonMargin.left() + - st::starrefButtonMargin.right()); + }, save->lifetime()); + save->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + const auto &margins = st::defaultBoxDividerLabelPadding; + raw->add( + object_ptr( + raw, + (_state->exists + ? tr::lng_star_ref_update_info + : tr::lng_star_ref_start_info)( + lt_terms, + tr::lng_star_ref_button_link() | Ui::Text::ToLink(), + Ui::Text::WithEntities), + st::boxDividerLabel), + QMargins(margins.left(), 0, margins.right(), 0)); + save->setClickedCallback([=] { + using Flag = MTPbots_UpdateStarRefProgram::Flag; + const auto weak = Ui::MakeWeak(this); + const auto user = _state->user; + auto program = StarRefProgram{ + .commission = _state->program.commission, + .durationMonths = _state->program.durationMonths, + }; + user->session().api().request(MTPbots_UpdateStarRefProgram( + MTP_flags((program.commission > 0 && program.durationMonths > 0) + ? Flag::f_duration_months + : Flag()), + user->inputUser, + MTP_int(program.commission), + MTP_int(program.durationMonths) + )).done([=] { + user->botInfo->starRefProgram.commission = program.commission; + user->botInfo->starRefProgram.durationMonths + = program.durationMonths; + if (weak) { + controller()->showBackFromStack(); + } + }).fail(crl::guard(weak, [=] { + controller()->showToast("Failed!"); + })).send(); + }); + + widthValue() | rpl::start_with_next([=](int width) { + raw->resizeToWidth(width); + }, raw->lifetime()); + + rpl::combine( + raw->heightValue(), + heightValue() + ) | rpl::start_with_next([=](int height, int fullHeight) { + setScrollBottomSkip(height); + raw->move(0, fullHeight - height); + }, raw->lifetime()); + + return result; +} + +std::shared_ptr Make(not_null peer) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(peer))); +} + +} // namespace Info::BotStarRef + diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_widget.h b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_widget.h new file mode 100644 index 000000000..bbcd4ae7c --- /dev/null +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_widget.h @@ -0,0 +1,82 @@ +/* +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 +*/ +#pragma once + +#include "info/info_content_widget.h" + +namespace Ui::Premium { +class TopBarAbstract; +} // namespace Ui::Premium + +namespace Ui { +template +class FadeWrap; +class IconButton; +} // namespace Ui + +namespace Info::BotStarRef { + +struct State; +class InnerWidget; + +class Memento final : public ContentMemento { +public: + Memento(not_null controller); + Memento(not_null peer); + ~Memento(); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + +}; + +class Widget final : public ContentWidget { +public: + Widget(QWidget *parent, not_null controller); + + bool showInternal(not_null memento) override; + rpl::producer title() override; + rpl::producer desiredShadowVisibility() const override; + void showFinished() override; + void setInnerFocus() override; + void enableBackButton() override; + + [[nodiscard]] not_null peer() const; + + void setInternalState( + const QRect &geometry, + not_null memento); + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + [[nodiscard]] std::unique_ptr setupTop(); + [[nodiscard]] std::unique_ptr setupBottom(); + + std::shared_ptr doCreateMemento() override; + + const not_null _inner; + const not_null _state; + + std::unique_ptr _top; + base::unique_qptr> _back; + base::unique_qptr _close; + rpl::variable _backEnabled; + + std::unique_ptr _bottom; + +}; + +[[nodiscard]] std::shared_ptr Make(not_null peer); + +} // namespace Info::BotStarRef diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 05796c024..fe90ea190 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1144,3 +1144,6 @@ infoHoursOuter: RoundButton(defaultActiveButton) { } infoHoursOuterMargin: margins(8px, 4px, 8px, 4px); infoHoursDaySkip: 6px; + +starrefInfoIconPosition: point(16px, 8px); +starrefButtonMargin: margins(12px, 12px, 12px, 12px); diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index 15583240a..b33417bab 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -374,10 +374,12 @@ Key ContentMemento::key() const { return Key(poll, pollContextId()); } else if (const auto self = settingsSelf()) { return Settings::Tag{ self }; - } else if (const auto peer = storiesPeer()) { - return Stories::Tag{ peer, storiesTab() }; - } else if (const auto peer = statisticsTag().peer) { + } else if (const auto stories = storiesPeer()) { + return Stories::Tag{ stories, storiesTab() }; + } else if (statisticsTag().peer) { return statisticsTag(); + } else if (const auto starref = starrefPeer()) { + return BotStarRef::Tag(starref); } else if (const auto who = reactionsWhoReadIds()) { return Key(who, _reactionsSelected, _pollReactionsContextId); } else { @@ -420,6 +422,10 @@ ContentMemento::ContentMemento(Statistics::Tag statistics) : _statisticsTag(statistics) { } +ContentMemento::ContentMemento(BotStarRef::Tag starref) +: _starrefPeer(starref.peer) { +} + ContentMemento::ContentMemento( std::shared_ptr whoReadIds, FullMsgId contextId, diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index f0c46da5a..12ac85074 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -52,6 +52,10 @@ namespace Info::Statistics { struct Tag; } // namespace Info::Statistics +namespace Info::BotStarRef { +struct Tag; +} // namespace Info::BotStarRef + namespace Info { class ContentMemento; @@ -191,6 +195,7 @@ public: explicit ContentMemento(Downloads::Tag downloads); explicit ContentMemento(Stories::Tag stories); explicit ContentMemento(Statistics::Tag statistics); + explicit ContentMemento(BotStarRef::Tag starref); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) , _pollReactionsContextId(contextId) { @@ -226,6 +231,9 @@ public: Statistics::Tag statisticsTag() const { return _statisticsTag; } + PeerData *starrefPeer() const { + return _starrefPeer; + } PollData *poll() const { return _poll; } @@ -280,6 +288,7 @@ private: PeerData * const _storiesPeer = nullptr; Stories::Tab _storiesTab = {}; Statistics::Tag _statisticsTag; + PeerData * const _starrefPeer = nullptr; PollData * const _poll = nullptr; std::shared_ptr _reactionsWhoReadIds; Data::ReactionId _reactionsSelected; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 388f870fe..38ef2852f 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -46,6 +46,9 @@ Key::Key(Stories::Tag stories) : _value(stories) { Key::Key(Statistics::Tag statistics) : _value(statistics) { } +Key::Key(BotStarRef::Tag starref) : _value(starref) { +} + Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } @@ -106,6 +109,13 @@ Statistics::Tag Key::statisticsTag() const { return Statistics::Tag(); } +PeerData *Key::starrefPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + PollData *Key::poll() const { if (const auto data = std::get_if(&_value)) { return data->poll; @@ -319,7 +329,8 @@ bool Controller::validateMementoPeer( && memento->migratedPeerId() == migratedPeerId() && memento->settingsSelf() == settingsSelf() && memento->storiesPeer() == storiesPeer() - && memento->statisticsTag().peer == statisticsTag().peer; + && memento->statisticsTag().peer == statisticsTag().peer + && memento->starrefPeer() == starrefPeer(); } void Controller::setSection(not_null memento) { diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 4fad8ca05..39f2d2bb1 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -61,6 +61,17 @@ struct Tag { } // namespace Info::Stories +namespace Info::BotStarRef { + +struct Tag { + explicit Tag(not_null peer) : peer(peer) { + } + + not_null peer; +}; + +} // namespace Info::BotStarRef + namespace Info { class Key { @@ -71,6 +82,7 @@ public: Key(Downloads::Tag downloads); Key(Stories::Tag stories); Key(Statistics::Tag statistics); + Key(BotStarRef::Tag starref); Key(not_null poll, FullMsgId contextId); Key( std::shared_ptr whoReadIds, @@ -84,6 +96,7 @@ public: PeerData *storiesPeer() const; Stories::Tab storiesTab() const; Statistics::Tag statisticsTag() const; + PeerData *starrefPeer() const; PollData *poll() const; FullMsgId pollContextId() const; std::shared_ptr reactionsWhoReadIds() const; @@ -107,6 +120,7 @@ private: Downloads::Tag, Stories::Tag, Statistics::Tag, + BotStarRef::Tag, PollKey, ReactionsKey> _value; @@ -134,6 +148,7 @@ public: Stories, PollResults, Statistics, + BotStarRef, Boosts, ChannelEarn, BotEarn, @@ -202,6 +217,9 @@ public: [[nodiscard]] Statistics::Tag statisticsTag() const { return key().statisticsTag(); } + [[nodiscard]] PeerData *starrefPeer() const { + return key().starrefPeer(); + } [[nodiscard]] PollData *poll() const; [[nodiscard]] FullMsgId pollContextId() const { return key().pollContextId(); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index a638537b1..4bd0dc12b 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -64,8 +64,9 @@ const style::InfoTopBar &TopBarStyle(Wrap wrap) { [[nodiscard]] bool HasCustomTopBar(not_null controller) { const auto section = controller->section(); - return (section.type() == Section::Type::Settings) - && section.settingsType()->hasCustomTopBar(); + return (section.type() == Section::Type::BotStarRef) + || ((section.type() == Section::Type::Settings) + && section.settingsType()->hasCustomTopBar()); } [[nodiscard]] Fn SelectedTitleForMedia( diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index e2b619273..e0bee03ee 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -138,6 +138,10 @@ TopBar::TopBar( _dollar = ScaleTo(QImage(u":/gui/art/business_logo.png"_q)); _ministars.setColorOverride( QGradientStops{{ 0, st::premiumButtonFg->c }}); + } else if (_logo == u"affiliate"_q) { + _dollar = ScaleTo(QImage(u":/gui/art/affiliate_logo.png"_q)); + _ministars.setColorOverride( + QGradientStops{{ 0, st::premiumButtonFg->c }}); } else if (!_light && !TopBarAbstract::isDark()) { _star.load(Svg()); _ministars.setColorOverride( diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index c81bedebb..7bb7b5302 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -42,6 +42,7 @@ menuIconRemove: icon {{ "menu/remove", menuIconColor }}; menuIconRetractVote: icon {{ "menu/retract_vote", menuIconColor }}; menuIconPermissions: icon {{ "menu/permissions", menuIconColor }}; menuIconShare: icon {{ "menu/share", menuIconColor }}; +menuIconSharing: icon {{ "menu/share2", menuIconColor }}; menuIconArchive: icon {{ "menu/archive", menuIconColor }}; menuIconUnarchive: icon {{ "menu/unarchive", menuIconColor }}; menuIconMarkRead: icon {{ "menu/read", menuIconColor }}; @@ -161,6 +162,7 @@ menuIconAppleWatch: icon {{ "menu/passcode_watch", menuIconColor }}; menuIconSystemPwd: menuIconPermissions; menuIconPlayerFullScreen: icon {{ "player/player_fullscreen", menuIconColor }}; menuIconPlayerWindowed: icon {{ "player/player_minimize", menuIconColor }}; +menuIconStarRefLink: icon{{ "settings/premium/features/feature_links2", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px);