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);