init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
/*
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
class PeerData;
namespace Data {
struct BoostPrepaidGiveaway;
} // namespace Data
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Ui {
class GenericBox;
} // namespace Ui
void CreateGiveawayBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Fn<void()> reloadOnDone,
std::optional<Data::BoostPrepaidGiveaway> prepaidGiveaway);

View File

@@ -0,0 +1,190 @@
/*
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/channel_statistics/boosts/giveaway/boost_badge.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "ui/widgets/labels.h"
#include "styles/style_giveaway.h"
#include "styles/style_statistics.h"
#include "styles/style_widgets.h"
namespace Info::Statistics {
not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
not_null<Ui::RpWidget*> parent,
int size,
const style::InfiniteRadialAnimation *st) {
class Widget final : public Ui::RpWidget {
public:
Widget(
not_null<Ui::RpWidget*> p,
int size,
const style::InfiniteRadialAnimation *st)
: Ui::RpWidget(p)
, _st(st ? st : &st::startGiveawayButtonLoading)
, _animation([=] { update(); }, *_st) {
resize(size, size);
shownValue() | rpl::on_next([=](bool v) {
return v
? _animation.start()
: _animation.stop(anim::type::instant);
}, lifetime());
}
protected:
void paintEvent(QPaintEvent *e) override {
auto p = QPainter(this);
p.setPen(st::activeButtonFg);
p.setBrush(st::activeButtonFg);
const auto r = rect() - Margins(_st->thickness);
_animation.draw(p, r.topLeft(), r.size(), width());
}
private:
const style::InfiniteRadialAnimation *_st;
Ui::InfiniteRadialAnimation _animation;
};
return Ui::CreateChild<Widget>(parent.get(), size, st);
}
void AddChildToWidgetCenter(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> child) {
parent->sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto size = child->size();
child->moveToLeft(
(s.width() - size.width()) / 2,
(s.height() - size.height()) / 2);
}, child->lifetime());
}
QImage CreateBadge(
const style::TextStyle &textStyle,
const QString &text,
int badgeHeight,
const style::margins &textPadding,
const style::color &bg,
const style::color &fg,
float64 bgOpacity,
const style::margins &iconPadding,
const style::icon &icon) {
auto badgeText = Ui::Text::String(textStyle, text);
const auto badgeTextWidth = badgeText.maxWidth();
const auto badgex = 0;
const auto badgey = 0;
const auto badgeh = 0 + badgeHeight;
const auto badgew = badgeTextWidth
+ rect::m::sum::h(textPadding);
auto result = QImage(
QSize(badgew, badgeh) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = Painter(&result);
p.setPen(Qt::NoPen);
p.setBrush(bg);
const auto r = QRect(badgex, badgey, badgew, badgeh);
{
auto hq = PainterHighQualityEnabler(p);
auto o = ScopedPainterOpacity(p, bgOpacity);
p.drawRoundedRect(r, badgeh / 2, badgeh / 2);
}
p.setPen(fg);
p.setBrush(Qt::NoBrush);
badgeText.drawLeftElided(
p,
r.x() + textPadding.left(),
badgey + textPadding.top(),
badgew,
badgew * 2);
icon.paint(
p,
QPoint(r.x() + iconPadding.left(), r.y() + iconPadding.top()),
badgew * 2);
}
return result;
}
void AddLabelWithBadgeToButton(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> text,
rpl::producer<int> number,
rpl::producer<bool> shown) {
struct State {
QImage badge;
};
const auto state = parent->lifetime().make_state<State>();
const auto label = Ui::CreateChild<Ui::LabelSimple>(
parent.get(),
st::startGiveawayButtonLabelSimple);
std::move(
text
) | rpl::on_next([=](const QString &s) {
label->setText(s);
}, label->lifetime());
const auto count = Ui::CreateChild<Ui::RpWidget>(parent.get());
count->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(count);
p.drawImage(0, 0, state->badge);
}, count->lifetime());
std::move(
number
) | rpl::on_next([=](int c) {
state->badge = Info::Statistics::CreateBadge(
st::startGiveawayButtonTextStyle,
QString::number(c),
st::boostsListBadgeHeight,
st::startGiveawayButtonBadgeTextPadding,
st::activeButtonFg,
st::activeButtonBg,
1.,
st::boostsListMiniIconPadding,
st::startGiveawayButtonMiniIcon);
count->resize(state->badge.size() / style::DevicePixelRatio());
count->update();
}, count->lifetime());
std::move(
shown
) | rpl::on_next([=](bool shown) {
count->setVisible(shown);
label->setVisible(shown);
}, count->lifetime());
rpl::combine(
parent->sizeValue(),
label->sizeValue(),
count->sizeValue()
) | rpl::on_next([=](
const QSize &s,
const QSize &s1,
const QSize &s2) {
const auto sum = st::startGiveawayButtonMiniIconSkip
+ s1.width()
+ s2.width();
const auto contentLeft = (s.width() - sum) / 2;
label->moveToLeft(contentLeft, (s.height() - s1.height()) / 2);
count->moveToLeft(
contentLeft + sum - s2.width(),
(s.height() - s2.height()) / 2 + st::boostsListMiniIconSkip);
}, parent->lifetime());
}
} // namespace Info::Statistics

View File

@@ -0,0 +1,47 @@
/*
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
namespace style {
struct InfiniteRadialAnimation;
struct TextStyle;
} // namespace style
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Info::Statistics {
[[nodiscard]] QImage CreateBadge(
const style::TextStyle &textStyle,
const QString &text,
int badgeHeight,
const style::margins &textPadding,
const style::color &bg,
const style::color &fg,
float64 bgOpacity,
const style::margins &iconPadding,
const style::icon &icon);
[[nodiscard]] not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
not_null<Ui::RpWidget*> parent,
int size,
const style::InfiniteRadialAnimation *st = nullptr);
void AddChildToWidgetCenter(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> child);
void AddLabelWithBadgeToButton(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> text,
rpl::producer<int> number,
rpl::producer<bool> shown);
} // namespace Info::Statistics

View File

@@ -0,0 +1,285 @@
/*
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
*/
using "ui/basic.style";
using "boxes/boxes.style";
using "ui/effects/premium.style";
using "statistics/statistics.style";
giveawayTypeListItem: PeerListItem(defaultPeerListItem) {
height: 52px;
photoPosition: point(58px, 6px);
namePosition: point(110px, 8px);
statusPosition: point(110px, 28px);
photoSize: 42px;
}
giveawayUserpic: icon {{ "boosts/filled_gift", windowFgActive }};
giveawayUserpicSkip: 1px;
giveawayUserpicGroup: icon {{ "limits/groups", windowFgActive }};
giveawayRadioPosition: point(21px, 16px);
giveawayGiftCodeCountryButton: SettingsButton(reportReasonButton) {
}
giveawayGiftCodeCountrySelect: MultiSelect(defaultMultiSelect) {
}
giveawayGiftCodeChannelDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }};
giveawayGiftCodeChannelDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }};
giveawayLoadingLabel: FlatLabel(membersAbout) {
}
giveawayGiftCodeTopHeight: 195px;
giveawayGiftCodeLink: FlatLabel(defaultFlatLabel) {
margin: margins(10px, 12px, 10px, 8px);
textFg: menuIconColor;
maxHeight: 24px;
}
giveawayGiftCodeLinkCopy: icon{{ "menu/copy", menuIconColor }};
giveawayGiftCodeLinkHeight: 42px;
giveawayGiftCodeLinkCopyWidth: 40px;
giveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px);
giveawayGiftCodeGiftOption: PremiumOption(premiumGiftOption) {
badgeShift: point(5px, 0px);
}
giveawayGiftCodeStartButton: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
radius: 6px;
}
giveawayGiftCodeQuantitySubtitle: FlatLabel(defaultFlatLabel) {
style: TextStyle(semiboldTextStyle) {
font: font(boxFontSize semibold);
}
textFg: windowActiveTextFg;
minWidth: 240px;
align: align(right);
}
giveawayGiftCodeQuantityFloat: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
textFg: windowActiveTextFg;
minWidth: 50px;
align: align(center);
}
boostLinkStatsButton: IconButton(defaultIconButton) {
width: giveawayGiftCodeLinkCopyWidth;
height: giveawayGiftCodeLinkHeight;
icon: icon{{ "menu/stats", menuIconColor }};
iconOver: icon{{ "menu/stats", menuIconColor }};
ripple: emptyRippleAnimation;
}
giveawayGiftCodeTable: Table(defaultTable) {
labelMinWidth: 91px;
}
giveawayGiftCodeTableMargin: margins(24px, 4px, 24px, 4px);
giveawayGiftCodeLabelMargin: margins(13px, 10px, 13px, 10px);
giveawayGiftCodeValueMultiline: FlatLabel(defaultTableValue) {
minWidth: 128px;
maxHeight: 100px;
style: TextStyle(defaultTextStyle) {
font: font(10px);
linkUnderline: kLinkUnderlineNever;
}
}
giveawayGiftMessage: FlatLabel(defaultTableValue) {
minWidth: 128px;
maxHeight: 0px;
}
giveawayGiftMessageRemove: IconButton(defaultIconButton) {
width: 32px;
height: 32px;
icon: icon{{ "menu/delete", windowActiveTextFg }};
iconOver: icon{{ "menu/delete", windowActiveTextFg }};
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 32px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgRipple;
}
}
giveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px);
giveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px);
giveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) {
size: size(24px, 24px);
photoSize: 24px;
photoPosition: point(-1px, -1px);
}
giveawayGiftCodeNamePosition: point(32px, 4px);
giveawayGiftCodeCover: PremiumCover(userPremiumCover) {
starSize: size(92px, 90px);
starTopSkip: 20px;
titlePadding: margins(0px, 15px, 0px, 17px);
titleFont: font(15px semibold);
about: FlatLabel(userPremiumCoverAbout) {
textFg: windowBoldFg;
style: TextStyle(premiumAboutTextStyle) {
lineHeight: 17px;
}
}
}
giveawayGiftCodeCoverClosePosition: point(5px, 0px);
giveawayGiftCodeCoverDividerPadding: margins(0px, 11px, 0px, 5px);
giveawayGiftCodeTypeDividerPadding: margins(0px, 7px, 0px, 5px);
giveawayGiftCodeSliderPadding: margins(0px, 24px, 0px, 10px);
giveawayGiftCodeSliderFloatSkip: 6px;
giveawayGiftCodeChannelsSubsectionPadding: margins(0px, -1px, 0px, -4px);
giveawayGiftCodeAdditionalPaddingMin: margins(50px, 4px, 22px, 0px);
giveawayGiftCodeAdditionalField: InputField(defaultMultiSelectSearchField) {
}
giveawayGiftCodeAdditionalLabel: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
}
giveawayGiftCodeAdditionalLabelSkip: 12px;
giveawayGiftCodeChannelsPeerList: PeerList(boostsListBox) {
padding: margins(0px, 7px, 0px, 0px);
}
giveawayGiftCodeMembersPeerList: PeerList(defaultPeerList) {
item: PeerListItem(defaultPeerListItem) {
height: 50px;
namePosition: point(62px, 7px);
statusPosition: point(62px, 27px);
}
}
giveawayRadioMembersPosition: point(21px, 14px);
giveawayGiftCodeChannelsAddButton: SettingsButton(defaultSettingsButton) {
textFg: lightButtonFg;
textFgOver: lightButtonFgOver;
padding: margins(70px, 10px, 22px, 8px);
iconLeft: 28px;
}
giveawayGiftCodeChannelsDividerPadding: margins(0px, 5px, 0px, 5px);
giveawayGiftCodeFooter: FlatLabel(defaultFlatLabel) {
align: align(top);
textFg: windowBoldFg;
}
giveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px);
giveawayGiftCodeBoxButton: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
style: semiboldTextStyle;
}
giveawayGiftCodeBox: Box(defaultBox) {
buttonPadding: margins(22px, 11px, 22px, 22px);
buttonHeight: 42px;
buttonWide: true;
button: giveawayGiftCodeBoxButton;
shadowIgnoreTopSkip: true;
}
giveawayGiftCodeBoxUpgradeNext: RoundButton(defaultLightButton, giveawayGiftCodeBoxButton) {
}
giveawayRefundedLabel: FlatLabel(boxLabel) {
align: align(top);
style: semiboldTextStyle;
textFg: attentionButtonFg;
}
giveawayRefundedPadding: margins(8px, 10px, 8px, 10px);
startGiveawayBox: Box(premiumGiftBox) {
shadowIgnoreTopSkip: true;
}
startGiveawayScrollArea: ScrollArea(boxScroll) {
deltax: 3px;
deltat: 50px;
}
startGiveawayBoxTitleClose: IconButton(boxTitleClose) {
ripple: universalRippleAnimation;
}
startGiveawayCover: PremiumCover(giveawayGiftCodeCover) {
bg: boxDividerBg;
additionalShadowForDarkThemes: false;
}
startGiveawayButtonLabelSimple: LabelSimple {
font: semiboldFont;
textFg: activeButtonFg;
}
startGiveawayButtonMiniIcon: icon{{ "boosts/boost_mini2", activeButtonBg }};
startGiveawayButtonMiniIconSkip: 5px;
startGiveawayButtonBadgeTextPadding: margins(16px, -1px, 6px, 0px);
startGiveawayButtonTextStyle: TextStyle(defaultTextStyle) {
font: semiboldFont;
}
startGiveawayButtonLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
color: activeButtonFg;
thickness: 2px;
}
starConvertButtonLoading: InfiniteRadialAnimation(startGiveawayButtonLoading) {
color: windowActiveTextFg;
thickness: 2px;
}
starGiftSmallButton: defaultTableSmallButton;
darkGiftCodeBox: Box(giveawayGiftCodeBox) {
bg: groupCallMembersBg;
title: FlatLabel(boxTitle) {
textFg: groupCallMembersFg;
}
titleAdditionalFg: groupCallMemberNotJoinedStatus;
}
darkGiftLink: icon {{ "menu/copy", groupCallMembersFg }};
darkGiftShare: icon {{ "menu/share", groupCallMembersFg }};
darkGiftTheme: icon {{ "menu/colors", groupCallMembersFg }};
darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }};
darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }};
darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }};
darkGiftNftResell: icon {{ "menu/tag_sell", groupCallMembersFg }};
darkGiftNftUnlist: icon {{ "menu/tag_remove", groupCallMembersFg }};
darkGiftHide: icon {{ "menu/stealth", groupCallMembersFg }};
darkGiftShow: icon {{ "menu/show_in_chat", groupCallMembersFg }};
darkGiftPin: icon {{ "menu/pin", groupCallMembersFg }};
darkGiftUnpin: icon {{ "menu/unpin", groupCallMembersFg }};
darkGiftOffer: icon {{ "menu/earn", groupCallMembersFg }};
darkGiftPalette: TextPalette(defaultTextPalette) {
linkFg: mediaviewTextLinkFg;
monoFg: groupCallMembersFg;
spoilerFg: groupCallMembersFg;
}
darkGiftTable: Table(giveawayGiftCodeTable) {
headerBg: groupCallMembersBgOver;
borderFg: mediaviewMenuBgOver;
smallButton: RoundButton(defaultTableSmallButton) {
textFg: groupCallMembersFg;
textFgOver: groupCallMembersFg;
textBg: groupCallMenuBgRipple;
textBgOver: groupCallMenuBgRipple;
ripple: RippleAnimation(defaultRippleAnimation) {
color: mediaviewMenuBgOver;
}
}
defaultLabel: FlatLabel(defaultTableLabel) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
defaultValue: FlatLabel(defaultTableValue) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
}
darkGiftTableValueMultiline: FlatLabel(giveawayGiftCodeValueMultiline) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
darkGiftTableMessage: FlatLabel(giveawayGiftMessage) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
darkGiftCodeLink: FlatLabel(giveawayGiftCodeLink) {
textFg: mediaviewMenuFg;
}
darkGiftBoxClose: IconButton(boxTitleClose) {
icon: icon {{ "box_button_close", groupCallMemberInactiveIcon }};
iconOver: icon {{ "box_button_close", groupCallMemberInactiveIcon }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: groupCallMembersBgOver;
}
}

View File

@@ -0,0 +1,375 @@
/*
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/channel_statistics/boosts/giveaway/giveaway_list_controllers.h"
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_folder.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "dialogs/dialogs_indexed_list.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "styles/style_giveaway.h"
namespace Giveaway {
namespace {
class ChannelRow final : public PeerListRow {
public:
using PeerListRow::PeerListRow;
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
void rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) override;
void rightActionStopLastRipple() override;
private:
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
};
QSize ChannelRow::rightActionSize() const {
return QSize(
st::giveawayGiftCodeChannelDeleteIcon.width(),
st::giveawayGiftCodeChannelDeleteIcon.height()) * 2;
}
QMargins ChannelRow::rightActionMargins() const {
const auto itemHeight = st::giveawayGiftCodeChannelsPeerList.item.height;
return QMargins(
0,
(itemHeight - rightActionSize().height()) / 2,
st::giveawayRadioPosition.x() / 2,
0);
}
void ChannelRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (_actionRipple) {
_actionRipple->paint(
p,
x,
y,
outerWidth);
if (_actionRipple->empty()) {
_actionRipple.reset();
}
}
const auto rect = QRect(QPoint(x, y), ChannelRow::rightActionSize());
(actionSelected
? st::giveawayGiftCodeChannelDeleteIconOver
: st::giveawayGiftCodeChannelDeleteIcon).paintInCenter(p, rect);
}
void ChannelRow::rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) {
if (!_actionRipple) {
auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());
_actionRipple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
std::move(updateCallback));
}
_actionRipple->add(point);
}
void ChannelRow::rightActionStopLastRipple() {
if (_actionRipple) {
_actionRipple->lastStop();
}
}
} // namespace
AwardMembersListController::AwardMembersListController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
std::vector<not_null<PeerData*>> selected)
: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members)
, _selected(std::move(selected)) {
}
void AwardMembersListController::prepare() {
ParticipantsBoxController::prepare();
delegate()->peerListAddSelectedPeers(base::take(_selected));
delegate()->peerListRefreshRows();
}
void AwardMembersListController::rowClicked(not_null<PeerListRow*> row) {
const auto checked = !row->checked();
if (checked
&& _checkErrorCallback
&& _checkErrorCallback(delegate()->peerListSelectedRowsCount())) {
return;
}
delegate()->peerListSetRowChecked(row, checked);
}
std::unique_ptr<PeerListRow> AwardMembersListController::createRow(
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (!user || user->isInaccessible() || user->isBot() || user->isSelf()) {
return nullptr;
}
return std::make_unique<PeerListRow>(participant);
}
base::unique_qptr<Ui::PopupMenu> AwardMembersListController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
return nullptr;
}
void AwardMembersListController::setCheckError(Fn<bool(int)> callback) {
_checkErrorCallback = std::move(callback);
}
MyChannelsListController::MyChannelsListController(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
std::vector<not_null<PeerData*>> selected)
: PeerListController(
std::make_unique<PeerListGlobalSearchController>(&peer->session()))
, _peer(peer)
, _show(show)
, _selected(std::move(selected))
, _otherChannels(std::make_unique<std::vector<not_null<ChannelData*>>>()) {
{
const auto addList = [&](not_null<Dialogs::IndexedList*> list) {
for (const auto &row : list->all()) {
if (const auto history = row->history()) {
const auto channel = history->peer->asChannel();
if (channel && !channel->isMegagroup()) {
_otherChannels->push_back(channel);
}
}
}
};
auto &data = _peer->owner();
addList(data.chatsList()->indexed());
if (const auto folder = data.folderLoaded(Data::Folder::kId)) {
addList(folder->chatsList()->indexed());
}
addList(data.contactsNoChatsList());
}
}
std::unique_ptr<PeerListRow> MyChannelsListController::createSearchRow(
not_null<PeerData*> peer) {
if (const auto channel = peer->asChannel()) {
return createRow(channel);
}
return nullptr;
}
std::unique_ptr<PeerListRow> MyChannelsListController::createRestoredRow(
not_null<PeerData*> peer) {
if (const auto channel = peer->asChannel()) {
return createRow(channel);
}
return nullptr;
}
void MyChannelsListController::loadMoreRows() {
if (_apiLifetime || !_otherChannels) {
return;
} else if (_lastAddedIndex >= _otherChannels->size()) {
_otherChannels.release();
return;
}
constexpr auto kPerPage = int(40);
const auto till = std::min(
int(_otherChannels->size()),
_lastAddedIndex + kPerPage);
while (_lastAddedIndex < till) {
delegate()->peerListAppendRow(
createRow(_otherChannels->at(_lastAddedIndex++)));
}
delegate()->peerListRefreshRows();
}
void MyChannelsListController::rowClicked(not_null<PeerListRow*> row) {
const auto channel = row->peer()->asChannel();
const auto checked = !row->checked();
if (checked
&& _checkErrorCallback
&& _checkErrorCallback(delegate()->peerListSelectedRowsCount())) {
return;
}
if (checked && channel && channel->username().isEmpty()) {
_show->showBox(Box(Ui::ConfirmBox, Ui::ConfirmBoxArgs{
.text = tr::lng_giveaway_channels_confirm_about(),
.confirmed = [=](Fn<void()> close) {
delegate()->peerListSetRowChecked(row, checked);
close();
},
.confirmText = tr::lng_filters_recommended_add(),
.title = tr::lng_giveaway_channels_confirm_title(),
}));
} else {
delegate()->peerListSetRowChecked(row, checked);
}
}
Main::Session &MyChannelsListController::session() const {
return _peer->session();
}
void MyChannelsListController::prepare() {
delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
const auto api = _apiLifetime.make_state<MTP::Sender>(
&session().api().instance());
api->request(
MTPstories_GetChatsToSend()
).done([=](const MTPmessages_Chats &result) {
_apiLifetime.destroy();
const auto &chats = result.match([](const auto &data) {
return data.vchats().v;
});
auto &owner = session().data();
for (const auto &chat : chats) {
if (const auto peer = owner.processChat(chat)) {
if (!peer->isChannel() || (peer == _peer)) {
continue;
}
if (!delegate()->peerListFindRow(peer->id.value)) {
if (const auto channel = peer->asChannel()) {
auto row = createRow(channel);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (ranges::contains(_selected, peer)) {
delegate()->peerListSetRowChecked(raw, true);
_selected.erase(
ranges::remove(_selected, peer),
end(_selected));
}
}
}
}
}
for (const auto &selected : _selected) {
if (const auto channel = selected->asChannel()) {
auto row = createRow(channel);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListSetRowChecked(raw, true);
}
}
delegate()->peerListRefreshRows();
_selected.clear();
}).send();
}
void MyChannelsListController::setCheckError(Fn<bool(int)> callback) {
_checkErrorCallback = std::move(callback);
}
std::unique_ptr<PeerListRow> MyChannelsListController::createRow(
not_null<ChannelData*> channel) const {
auto row = std::make_unique<PeerListRow>(channel);
row->setCustomStatus((channel->isBroadcast()
? tr::lng_chat_status_subscribers
: tr::lng_chat_status_members)(
tr::now,
lt_count,
channel->membersCount()));
return row;
}
SelectedChannelsListController::SelectedChannelsListController(
not_null<PeerData*> peer)
: _peer(peer) {
PeerListController::setStyleOverrides(
&st::giveawayGiftCodeChannelsPeerList);
}
void SelectedChannelsListController::setTopStatus(rpl::producer<QString> s) {
_statusLifetime = std::move(
s
) | rpl::on_next([=](const QString &t) {
if (delegate()->peerListFullRowsCount() > 0) {
delegate()->peerListRowAt(0)->setCustomStatus(t);
}
});
}
void SelectedChannelsListController::rebuild(
std::vector<not_null<PeerData*>> selected) {
while (delegate()->peerListFullRowsCount() > 1) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(1));
}
for (const auto &peer : selected) {
delegate()->peerListAppendRow(createRow(peer->asChannel()));
}
delegate()->peerListRefreshRows();
}
auto SelectedChannelsListController::channelRemoved() const
-> rpl::producer<not_null<PeerData*>> {
return _channelRemoved.events();
}
void SelectedChannelsListController::rowClicked(not_null<PeerListRow*> row) {
}
void SelectedChannelsListController::rowRightActionClicked(
not_null<PeerListRow*> row) {
const auto peer = row->peer();
delegate()->peerListRemoveRow(row);
delegate()->peerListRefreshRows();
_channelRemoved.fire_copy(peer);
}
Main::Session &SelectedChannelsListController::session() const {
return _peer->session();
}
void SelectedChannelsListController::prepare() {
delegate()->peerListAppendRow(createRow(_peer->asChannel()));
}
std::unique_ptr<PeerListRow> SelectedChannelsListController::createRow(
not_null<ChannelData*> channel) const {
const auto isYourChannel = (_peer->asChannel() == channel);
auto row = isYourChannel
? std::make_unique<PeerListRow>(channel)
: std::make_unique<ChannelRow>(channel);
row->setCustomStatus(isYourChannel
? QString()
: (channel->isMegagroup()
? tr::lng_chat_status_members
: tr::lng_chat_status_subscribers)(
tr::now,
lt_count,
channel->membersCount()));
return row;
}
} // namespace Giveaway

View File

@@ -0,0 +1,113 @@
/*
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 "boxes/peers/edit_participants_box.h"
class ChannelData;
class PeerData;
class PeerListRow;
namespace Ui {
class PopupMenu;
class Show;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Giveaway {
class AwardMembersListController : public ParticipantsBoxController {
public:
AwardMembersListController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
std::vector<not_null<PeerData*>> selected);
void prepare() override;
void setCheckError(Fn<bool(int)> callback);
void rowClicked(not_null<PeerListRow*> row) override;
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
private:
Fn<bool(int)> _checkErrorCallback;
std::vector<not_null<PeerData*>> _selected;
};
class MyChannelsListController : public PeerListController {
public:
MyChannelsListController(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
std::vector<not_null<PeerData*>> selected);
void setCheckError(Fn<bool(int)> callback);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
private:
std::unique_ptr<PeerListRow> createRow(
not_null<ChannelData*> channel) const;
const not_null<PeerData*> _peer;
const std::shared_ptr<Ui::Show> _show;
Fn<bool(int)> _checkErrorCallback;
std::vector<not_null<PeerData*>> _selected;
std::unique_ptr<std::vector<not_null<ChannelData*>>> _otherChannels;
int _lastAddedIndex = 0;
rpl::lifetime _apiLifetime;
};
class SelectedChannelsListController : public PeerListController {
public:
SelectedChannelsListController(not_null<PeerData*> peer);
void setTopStatus(rpl::producer<QString> status);
void rebuild(std::vector<not_null<PeerData*>> selected);
[[nodiscard]] rpl::producer<not_null<PeerData*>> channelRemoved() const;
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
private:
std::unique_ptr<PeerListRow> createRow(
not_null<ChannelData*> channel) const;
const not_null<PeerData*> _peer;
rpl::event_stream<not_null<PeerData*>> _channelRemoved;
rpl::lifetime _statusLifetime;
};
} // namespace Giveaway

View File

@@ -0,0 +1,194 @@
/*
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/channel_statistics/boosts/giveaway/giveaway_type_row.h"
#include "lang/lang_keys.h"
#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_options.h"
#include "ui/widgets/checkbox.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_color_indices.h"
#include "styles/style_giveaway.h"
#include "styles/style_statistics.h"
namespace Giveaway {
GiveawayTypeRow::GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
rpl::producer<QString> subtitle,
bool group)
: GiveawayTypeRow(
parent,
type,
(type == Type::SpecificUsers) ? st::colorIndexBlue : st::colorIndexGreen,
(type == Type::SpecificUsers)
? tr::lng_giveaway_award_option()
: (type == Type::Random)
? tr::lng_premium_summary_title()
// ? tr::lng_giveaway_create_option()
: (type == Type::AllMembers)
? (group
? tr::lng_giveaway_users_all_group()
: tr::lng_giveaway_users_all())
: (group
? tr::lng_giveaway_users_new_group()
: tr::lng_giveaway_users_new()),
std::move(subtitle),
QImage()) {
}
GiveawayTypeRow::GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
int colorIndex,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
QImage badge)
: RippleButton(parent, st::defaultRippleAnimation)
, _type(type)
, _st((_type == Type::SpecificUsers
|| _type == Type::Random
|| _type == Type::Credits)
? st::giveawayTypeListItem
: ((_type == Type::Prepaid) || (_type == Type::PrepaidCredits))
? st::boostsListBox.item
: st::giveawayGiftCodeMembersPeerList.item)
, _userpic(
Ui::EmptyUserpic::UserpicColor(Ui::EmptyUserpic::ColorIndex(colorIndex)),
QString())
, _badge(std::move(badge)) {
if (_type == Type::Credits || _type == Type::PrepaidCredits) {
_customUserpic = Ui::CreditsWhiteDoubledIcon(_st.photoSize, 1.);
}
std::move(
subtitle
) | rpl::on_next([=] (QString s) {
_status.setText(
st::defaultTextStyle,
s.replace(QChar('>'), QString()),
Ui::NameTextOptions());
}, lifetime());
std::move(
title
) | rpl::on_next([=] (const QString &s) {
_name.setText(_st.nameStyle, s, Ui::NameTextOptions());
}, lifetime());
}
int GiveawayTypeRow::resizeGetHeight(int) {
return _st.height;
}
void GiveawayTypeRow::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto paintOver = (isOver() || isDown()) && !isDisabled();
const auto skipRight = _st.photoPosition.x();
const auto outerWidth = width();
const auto isRandom = (_type == Type::Random);
const auto isSpecific = (_type == Type::SpecificUsers);
const auto isPrepaid = (_type == Type::Prepaid);
const auto hasUserpic = isRandom
|| isSpecific
|| isPrepaid
|| (!_customUserpic.isNull());
if (paintOver) {
p.fillRect(e->rect(), _st.button.textBgOver);
}
Ui::RippleButton::paintRipple(p, 0, 0);
if (hasUserpic) {
_userpic.paintCircle(
p,
_st.photoPosition.x(),
_st.photoPosition.y(),
outerWidth,
_st.photoSize);
const auto userpicRect = QRect(
_st.photoPosition
- QPoint(
isSpecific ? -st::giveawayUserpicSkip : 0,
isSpecific ? 0 : st::giveawayUserpicSkip),
Size(_st.photoSize));
if (!_customUserpic.isNull()) {
p.drawImage(_st.photoPosition, _customUserpic);
} else {
const auto &userpic = isSpecific
? st::giveawayUserpicGroup
: st::giveawayUserpic;
userpic.paintInCenter(p, userpicRect);
}
}
const auto namex = _st.namePosition.x();
const auto namey = _st.namePosition.y();
const auto namew = outerWidth - namex - skipRight;
const auto badgew = _badge.width() / style::DevicePixelRatio();
p.setPen(_st.nameFg);
_name.drawLeftElided(p, namex, namey, namew - badgew, width());
if (!_badge.isNull()) {
p.drawImage(
std::min(
namex + _name.maxWidth() + st::boostsListBadgePadding.left(),
outerWidth - badgew - skipRight),
namey + st::boostsListMiniIconSkip,
_badge);
}
const auto statusIcon = isRandom ? &st::topicButtonArrow : nullptr;
const auto statusx = _st.statusPosition.x();
const auto statusy = _st.statusPosition.y();
const auto statusw = outerWidth
- statusx
- skipRight
- (statusIcon
? (statusIcon->width() + st::boostsListMiniIconSkip)
: 0);
p.setFont(st::contactsStatusFont);
p.setPen((isRandom || !hasUserpic) ? st::lightButtonFg : _st.statusFg);
_status.drawLeftElided(p, statusx, statusy, statusw, outerWidth);
if (statusIcon) {
statusIcon->paint(
p,
QPoint(
statusx
+ std::min(_status.maxWidth(), statusw)
+ st::boostsListMiniIconSkip,
statusy + st::contactsStatusFont->descent),
outerWidth,
st::lightButtonFg->c);
}
}
void GiveawayTypeRow::addRadio(
std::shared_ptr<Ui::RadioenumGroup<Type>> typeGroup) {
const auto &st = st::defaultCheckbox;
const auto radio = Ui::CreateChild<Ui::Radioenum<Type>>(
this,
std::move(typeGroup),
_type,
QString(),
st);
const auto pos = (_type == Type::SpecificUsers || _type == Type::Random)
? st::giveawayRadioPosition
: st::giveawayRadioMembersPosition;
radio->moveToLeft(pos.x(), pos.y());
radio->setAttribute(Qt::WA_TransparentForMouseEvents);
radio->show();
}
} // namespace Giveaway

View File

@@ -0,0 +1,70 @@
/*
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 "ui/empty_userpic.h"
#include "ui/widgets/buttons.h"
namespace Ui {
template <typename Enum>
class RadioenumGroup;
} // namespace Ui
namespace Giveaway {
class GiveawayTypeRow final : public Ui::RippleButton {
public:
enum class Type {
Random,
SpecificUsers,
AllMembers,
OnlyNewMembers,
Prepaid,
PrepaidCredits,
Credits,
};
GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
rpl::producer<QString> subtitle,
bool group);
GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
int colorIndex,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
QImage badge);
void addRadio(std::shared_ptr<Ui::RadioenumGroup<Type>> typeGroup);
protected:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int) override;
private:
const Type _type;
const style::PeerListItem _st;
Ui::EmptyUserpic _userpic;
Ui::Text::String _status;
Ui::Text::String _name;
QImage _customUserpic;
QImage _badge;
};
} // namespace Giveaway

View File

@@ -0,0 +1,220 @@
/*
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/channel_statistics/boosts/giveaway/select_countries_box.h"
#include "countries/countries_instance.h"
#include "lang/lang_keys.h"
#include "ui/emoji_config.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/multi_select.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_giveaway.h"
#include "styles/style_settings.h"
namespace Ui {
namespace {
[[nodiscard]] QImage CacheFlagEmoji(const QString &flag) {
const auto &st = st::giveawayGiftCodeCountrySelect.item;
auto roundPaintCache = QImage(
Size(st.height) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
roundPaintCache.setDevicePixelRatio(style::DevicePixelRatio());
roundPaintCache.fill(Qt::transparent);
{
const auto size = st.height;
auto p = Painter(&roundPaintCache);
auto hq = PainterHighQualityEnabler(p);
const auto flagText = Ui::Text::String(st::defaultTextStyle, flag);
p.setPen(st.textBg);
p.setBrush(st.textBg);
p.drawEllipse(0, 0, size, size);
flagText.draw(p, {
.position = QPoint(
0 + (size - flagText.maxWidth()) / 2,
0 + (size - flagText.minHeight()) / 2),
.outerWidth = size,
.availableWidth = size,
});
}
return roundPaintCache;
}
} // namespace
void SelectCountriesBox(
not_null<Ui::GenericBox*> box,
const std::vector<QString> &selected,
Fn<void(std::vector<QString>)> doneCallback,
Fn<bool(int)> checkErrorCallback) {
struct State final {
std::vector<QString> resultList;
};
const auto state = box->lifetime().make_state<State>();
const auto multiSelect = box->setPinnedToTopContent(
object_ptr<Ui::MultiSelect>(
box,
st::giveawayGiftCodeCountrySelect,
tr::lng_participant_filter()));
Ui::AddSkip(box->verticalLayout());
const auto &buttonSt = st::giveawayGiftCodeCountryButton;
struct Entry final {
Ui::SlideWrap<Ui::SettingsButton> *wrap = nullptr;
QStringList list;
QString iso2;
};
auto countries = Countries::Instance().list();
ranges::sort(countries, [](
const Countries::Info &a,
const Countries::Info &b) {
return (a.name.compare(b.name, Qt::CaseInsensitive) < 0);
});
auto buttons = std::vector<Entry>();
buttons.reserve(countries.size());
for (const auto &country : countries) {
const auto flag = Countries::Instance().flagEmojiByISO2(country.iso2);
if (!Ui::Emoji::Find(flag)) {
continue;
}
const auto itemId = buttons.size();
auto button = object_ptr<SettingsButton>(
box->verticalLayout(),
rpl::single(flag + ' ' + country.name),
buttonSt);
const auto radio = Ui::CreateChild<Ui::RpWidget>(button.data());
const auto radioView = std::make_shared<Ui::RadioView>(
st::defaultRadio,
false,
[=] { radio->update(); });
{
const auto radioSize = radioView->getSize();
radio->resize(radioSize);
radio->paintRequest(
) | rpl::on_next([=](const QRect &r) {
auto p = QPainter(radio);
radioView->paint(p, 0, 0, radioSize.width());
}, radio->lifetime());
const auto buttonHeight = buttonSt.height
+ rect::m::sum::v(buttonSt.padding);
radio->moveToLeft(
st::giveawayRadioPosition.x(),
(buttonHeight - radioSize.height()) / 2);
}
const auto roundPaintCache = CacheFlagEmoji(flag);
const auto paintCallback = [=](Painter &p, int x, int y, int, int) {
p.drawImage(x, y, roundPaintCache);
};
const auto choose = [=](bool clicked) {
const auto value = !radioView->checked();
if (value && checkErrorCallback(state->resultList.size())) {
return;
}
radioView->setChecked(value, anim::type::normal);
if (value) {
state->resultList.push_back(country.iso2);
multiSelect->addItem(
itemId,
country.name,
st::activeButtonBg,
paintCallback,
clicked
? Ui::MultiSelect::AddItemWay::Default
: Ui::MultiSelect::AddItemWay::SkipAnimation);
} else {
auto &list = state->resultList;
list.erase(ranges::remove(list, country.iso2), end(list));
multiSelect->removeItem(itemId);
}
};
button->setClickedCallback([=] {
choose(true);
});
if (ranges::contains(selected, country.iso2)) {
choose(false);
}
const auto wrap = box->verticalLayout()->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
box,
std::move(button)));
wrap->toggle(true, anim::type::instant);
{
auto list = QStringList{
flag,
country.name,
country.alternativeName,
};
buttons.push_back({ wrap, std::move(list), country.iso2 });
}
}
const auto noResults = box->addRow(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)));
{
noResults->toggle(false, anim::type::instant);
const auto container = noResults->entity();
Ui::AddSkip(container);
Ui::AddSkip(container);
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_search_messages_none(),
st::membersAbout),
style::al_top);
Ui::AddSkip(container);
Ui::AddSkip(container);
}
multiSelect->setQueryChangedCallback([=](const QString &query) {
auto wasAnyFound = false;
for (const auto &entry : buttons) {
const auto found = ranges::any_of(entry.list, [&](
const QString &s) {
return s.startsWith(query, Qt::CaseInsensitive);
});
entry.wrap->toggle(found, anim::type::instant);
wasAnyFound |= found;
}
noResults->toggle(!wasAnyFound, anim::type::instant);
});
multiSelect->setItemRemovedCallback([=](uint64 itemId) {
auto &list = state->resultList;
auto &button = buttons[itemId];
const auto it = ranges::find(list, button.iso2);
if (it != end(list)) {
list.erase(it);
button.wrap->entity()->clicked({}, Qt::LeftButton);
}
});
box->addButton(tr::lng_settings_save(), [=] {
doneCallback(state->resultList);
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
} // namespace Ui

View File

@@ -0,0 +1,20 @@
/*
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
namespace Ui {
class GenericBox;
void SelectCountriesBox(
not_null<Ui::GenericBox*> box,
const std::vector<QString> &selected,
Fn<void(std::vector<QString>)> doneCallback,
Fn<bool(int)> checkErrorCallback);
} // namespace Ui

View File

@@ -0,0 +1,571 @@
/*
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/channel_statistics/boosts/info_boosts_inner_widget.h"
#include "api/api_premium.h"
#include "api/api_statistics.h"
#include "boxes/gift_premium_box.h"
#include "boxes/peers/edit_peer_invite_link.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/channel_statistics/boosts/create_giveaway_box.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
#include "info/channel_statistics/boosts/giveaway/giveaway_type_row.h"
#include "info/channel_statistics/boosts/info_boosts_widget.h"
#include "info/info_controller.h"
#include "info/profile/info_profile_icon.h"
#include "info/statistics/info_statistics_inner_widget.h" // FillLoading.
#include "info/statistics/info_statistics_list_controllers.h"
#include "lang/lang_keys.h"
#include "settings/settings_credits_graphics.h"
#include "statistics/widgets/chart_header_widget.h"
#include "ui/boxes/boost_box.h"
#include "ui/controls/invite_link_label.h"
#include "ui/effects/ripple_animation.h"
#include "ui/empty_userpic.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/slider_natural_width.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/ui_utility.h"
#include "styles/style_color_indices.h"
#include "styles/style_dialogs.h" // dialogsSearchTabs
#include "styles/style_giveaway.h"
#include "styles/style_info.h"
#include "styles/style_premium.h"
#include "styles/style_statistics.h"
#include <QtGui/QGuiApplication>
namespace Info::Boosts {
namespace {
void AddHeader(
not_null<Ui::VerticalLayout*> content,
tr::phrase<> text) {
const auto header = content->add(
object_ptr<Statistic::Header>(content),
st::statisticsLayerMargins + st::boostsChartHeaderPadding);
header->resizeToWidth(header->width());
header->setTitle(text(tr::now));
header->setSubTitle({});
}
void FillOverview(
not_null<Ui::VerticalLayout*> content,
const Data::BoostStatus &status) {
const auto &stats = status.overview;
Ui::AddSkip(content, st::boostsLayerOverviewMargins.top());
AddHeader(content, tr::lng_stats_overview_title);
Ui::AddSkip(content);
const auto diffBetweenHeaders = 0
+ st::statisticsOverviewValue.style.font->height
- st::statisticsHeaderTitleTextStyle.font->height;
const auto container = content->add(
object_ptr<Ui::RpWidget>(content),
st::statisticsLayerMargins);
const auto addPrimary = [&](float64 v, bool approximately = false) {
return Ui::CreateChild<Ui::FlatLabel>(
container,
(v >= 0)
? (approximately && v ? QChar(0x2248) : QChar())
+ Lang::FormatCountToShort(v).string
: QString(),
st::statisticsOverviewValue);
};
const auto addSub = [&](
not_null<Ui::RpWidget*> primary,
float64 percentage,
tr::phrase<> text) {
const auto second = Ui::CreateChild<Ui::FlatLabel>(
container,
percentage
? u"%1%"_q.arg(std::abs(std::round(percentage * 10.) / 10.))
: QString(),
st::statisticsOverviewSecondValue);
second->setTextColorOverride(st::windowSubTextFg->c);
const auto sub = Ui::CreateChild<Ui::FlatLabel>(
container,
text(),
st::statisticsOverviewSubtext);
sub->setTextColorOverride(st::windowSubTextFg->c);
primary->geometryValue(
) | rpl::on_next([=](const QRect &g) {
const auto &padding = st::statisticsOverviewSecondValuePadding;
second->moveToLeft(
rect::right(g) + padding.left(),
g.y() + padding.top());
sub->moveToLeft(
g.x(),
st::statisticsChartHeaderHeight
- st::statisticsOverviewSubtext.style.font->height
+ g.y()
+ diffBetweenHeaders);
}, primary->lifetime());
};
const auto topLeftLabel = addPrimary(stats.level);
const auto topRightLabel = addPrimary(stats.premiumMemberCount, true);
const auto bottomLeftLabel = addPrimary(stats.boostCount);
const auto bottomRightLabel = addPrimary(std::max(
stats.nextLevelBoostCount - stats.boostCount,
0));
addSub(
topLeftLabel,
0,
tr::lng_boosts_level);
addSub(
topRightLabel,
stats.premiumMemberPercentage,
(stats.group
? tr::lng_boosts_premium_members
: tr::lng_boosts_premium_audience));
addSub(
bottomLeftLabel,
0,
tr::lng_boosts_existing);
addSub(
bottomRightLabel,
0,
tr::lng_boosts_next_level);
container->showChildren();
container->resize(container->width(), topLeftLabel->height() * 5);
container->sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto halfWidth = s.width() / 2;
{
const auto &p = st::boostsOverviewValuePadding;
topLeftLabel->moveToLeft(p.left(), p.top());
}
topRightLabel->moveToLeft(
topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip,
topLeftLabel->y());
bottomLeftLabel->moveToLeft(
topLeftLabel->x(),
topLeftLabel->y() + st::statisticsOverviewMidSkip);
bottomRightLabel->moveToLeft(
topRightLabel->x(),
bottomLeftLabel->y());
}, container->lifetime());
Ui::AddSkip(content, st::boostsLayerOverviewMargins.bottom());
}
void FillShareLink(
not_null<Ui::VerticalLayout*> content,
std::shared_ptr<Ui::Show> show,
const QString &link,
not_null<PeerData*> peer) {
const auto weak = base::make_weak(content);
const auto copyLink = crl::guard(weak, [=] {
QGuiApplication::clipboard()->setText(link);
show->showToast(tr::lng_channel_public_link_copied(tr::now));
});
const auto shareLink = crl::guard(weak, [=] {
show->showBox(ShareInviteLinkBox(peer, link));
});
const auto label = content->lifetime().make_state<Ui::InviteLinkLabel>(
content,
rpl::single(base::duplicate(link).replace(u"https://"_q, QString())),
nullptr);
content->add(
label->take(),
st::boostsLinkFieldPadding);
label->clicks(
) | rpl::on_next(copyLink, label->lifetime());
{
const auto wrap = content->add(
object_ptr<Ui::FixedHeightWidget>(
content,
st::inviteLinkButton.height),
st::inviteLinkButtonsPadding);
const auto copy = CreateChild<Ui::RoundButton>(
wrap,
tr::lng_group_invite_context_copy(),
st::inviteLinkCopy);
copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
copy->setClickedCallback(copyLink);
const auto share = CreateChild<Ui::RoundButton>(
wrap,
tr::lng_group_invite_context_share(),
st::inviteLinkShare);
share->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
share->setClickedCallback(shareLink);
wrap->widthValue(
) | rpl::on_next([=](int width) {
const auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2;
copy->setFullWidth(buttonWidth);
share->setFullWidth(buttonWidth);
copy->moveToLeft(0, 0, width);
share->moveToRight(0, 0, width);
}, wrap->lifetime());
wrap->showChildren();
}
Ui::AddSkip(content, st::boostsLinkFieldPadding.bottom());
}
void FillGetBoostsButton(
not_null<Ui::VerticalLayout*> content,
not_null<Controller*> controller,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
Fn<void()> reloadOnDone) {
if (!Api::PremiumGiftCodeOptions(peer).giveawayGiftsPurchaseAvailable()) {
return;
}
Ui::AddSkip(content);
const auto &st = st::getBoostsButton;
const auto &icon = st::getBoostsButtonIcon;
const auto button = content->add(object_ptr<Ui::SettingsButton>(
content.get(),
tr::lng_boosts_get_boosts(),
st));
button->setClickedCallback([=] {
show->showBox(Box(
CreateGiveawayBox,
controller,
peer,
reloadOnDone,
std::nullopt));
});
Ui::CreateChild<Info::Profile::FloatingIcon>(
button,
icon,
QPoint{
st::infoSharedMediaButtonIconPosition.x(),
(st.height + rect::m::sum::v(st.padding) - icon.height()) / 2,
})->show();
Ui::AddSkip(content);
Ui::AddDividerText(
content,
peer->isMegagroup()
? tr::lng_boosts_get_boosts_subtext_group()
: tr::lng_boosts_get_boosts_subtext());
}
} // namespace
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PeerData*> peer)
: VerticalLayout(parent)
, _controller(controller)
, _peer(peer)
, _show(controller->uiShow()) {
}
void InnerWidget::load() {
const auto api = lifetime().make_state<Api::Boosts>(_peer);
Info::Statistics::FillLoading(
this,
Info::Statistics::LoadingType::Boosts,
_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),
_showFinished.events());
_showFinished.events(
) | rpl::take(1) | rpl::on_next([=] {
api->request(
) | rpl::on_error_done([](const QString &error) {
}, [=] {
_state = api->boostStatus();
_loaded.fire(true);
fill();
}, lifetime());
}, lifetime());
}
void InnerWidget::fill() {
const auto fakeShowed = lifetime().make_state<rpl::event_stream<>>();
const auto &status = _state;
const auto inner = this;
const auto reloadOnDone = crl::guard(this, [=] {
while (Ui::VerticalLayout::count()) {
delete Ui::VerticalLayout::widgetAt(0);
}
load();
_showFinished.fire({});
});
{
auto dividerContent = object_ptr<Ui::VerticalLayout>(inner);
dividerContent->add(object_ptr<Ui::FixedHeightWidget>(
dividerContent,
st::boostSkipTop));
Ui::FillBoostLimit(
fakeShowed->events(),
dividerContent.data(),
rpl::single(Ui::BoostCounters{
.level = status.overview.level,
.boosts = status.overview.boostCount,
.thisLevelBoosts
= status.overview.currentLevelBoostCount,
.nextLevelBoosts
= status.overview.nextLevelBoostCount,
.mine = status.overview.mine,
}),
st::statisticsLimitsLinePadding);
inner->add(object_ptr<Ui::DividerLabel>(
inner,
std::move(dividerContent),
st::statisticsLimitsDividerPadding));
}
FillOverview(inner, status);
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
if (!status.prepaidGiveaway.empty()) {
const auto multiplier = Api::PremiumGiftCodeOptions(_peer)
.giveawayBoostsPerPremium();
Ui::AddSkip(inner);
AddHeader(inner, tr::lng_boosts_prepaid_giveaway_title);
Ui::AddSkip(inner);
for (const auto &g : status.prepaidGiveaway) {
using namespace Giveaway;
const auto button = inner->add(object_ptr<GiveawayTypeRow>(
inner,
g.credits
? GiveawayTypeRow::Type::PrepaidCredits
: GiveawayTypeRow::Type::Prepaid,
g.credits ? st::colorIndexOrange : g.id,
g.credits
? tr::lng_boosts_prepaid_giveaway_single()
: tr::lng_boosts_prepaid_giveaway_quantity(
lt_count,
rpl::single(g.quantity) | tr::to_count()),
g.credits
? tr::lng_boosts_prepaid_giveaway_credits_status(
lt_count,
rpl::single(g.quantity) | tr::to_count(),
lt_amount,
tr::lng_prize_credits_amount(
lt_count_decimal,
rpl::single(g.credits) | tr::to_count()))
: tr::lng_boosts_prepaid_giveaway_moths(
lt_count,
rpl::single(g.months) | tr::to_count()),
Info::Statistics::CreateBadge(
st::statisticsDetailsBottomCaptionStyle,
QString::number(
g.boosts ? g.boosts : (g.quantity * multiplier)),
st::boostsListBadgeHeight,
st::boostsListBadgeTextPadding,
st::premiumButtonBg2,
st::premiumButtonFg,
1.,
st::boostsListMiniIconPadding,
st::boostsListMiniIcon)));
button->setClickedCallback([=] {
_controller->uiShow()->showBox(Box(
CreateGiveawayBox,
_controller,
_peer,
reloadOnDone,
g));
});
}
Ui::AddSkip(inner);
Ui::AddDividerText(
inner,
tr::lng_boosts_prepaid_giveaway_title_subtext());
Ui::AddSkip(inner);
}
const auto hasBoosts = (status.firstSliceBoosts.multipliedTotal > 0);
const auto hasGifts = (status.firstSliceGifts.multipliedTotal > 0);
if (hasBoosts || hasGifts) {
auto boostClicked = [=](const Data::Boost &boost) {
if (!boost.giftCodeLink.slug.isEmpty()) {
ResolveGiftCode(_controller, boost.giftCodeLink.slug);
} else if (boost.userId) {
const auto user = _peer->owner().user(boost.userId);
if (boost.isGift || boost.isGiveaway) {
const auto d = Api::GiftCode{
.from = _peer->id,
.to = user->id,
.date = TimeId(boost.date.toSecsSinceEpoch()),
.days = boost.expiresAfterMonths * 30,
};
_show->showBox(Box(GiftCodePendingBox, _controller, d));
} else {
crl::on_main(this, [=] {
_controller->showPeerInfo(user);
});
}
} else if (boost.credits) {
_show->showBox(
Box(
::Settings::BoostCreditsBox,
_controller->parentController(),
boost));
} else if (!boost.isUnclaimed) {
_show->showToast(tr::lng_boosts_list_pending_about(tr::now));
}
};
#ifdef _DEBUG
const auto hasOneTab = false;
#else
const auto hasOneTab = (hasBoosts != hasGifts);
#endif
const auto boostsTabText = tr::lng_giveaway_quantity(
tr::now,
lt_count,
status.firstSliceBoosts.multipliedTotal);
const auto giftsTabText = tr::lng_boosts_list_tab_gifts(
tr::now,
lt_count,
status.firstSliceGifts.multipliedTotal);
if (hasOneTab) {
Ui::AddSkip(inner);
const auto header = inner->add(
object_ptr<Statistic::Header>(inner),
st::statisticsLayerMargins
+ st::boostsChartHeaderPadding);
header->resizeToWidth(header->width());
header->setTitle(hasBoosts ? boostsTabText : giftsTabText);
header->setSubTitle({});
}
const auto slider = inner->add(
object_ptr<Ui::SlideWrap<Ui::CustomWidthSlider>>(
inner,
object_ptr<Ui::CustomWidthSlider>(
inner,
st::dialogsSearchTabs)));
if (!hasOneTab) {
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(inner);
shadow->show();
slider->geometryValue(
) | rpl::on_next([=](const QRect &r) {
shadow->setGeometry(
inner->x(),
rect::bottom(r) - shadow->height(),
inner->width(),
shadow->height());
}, shadow->lifetime());
}
slider->toggle(!hasOneTab, anim::type::instant);
slider->entity()->addSection(boostsTabText);
slider->entity()->addSection(giftsTabText);
{
const auto &st = st::defaultTabsSlider;
slider->entity()->setNaturalWidth(0
+ st.labelStyle.font->width(boostsTabText)
+ st.labelStyle.font->width(giftsTabText)
+ rect::m::sum::h(st::boxRowPadding));
}
const auto boostsWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto giftsWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
rpl::single(hasOneTab ? (hasGifts ? 1 : 0) : 0) | rpl::then(
slider->entity()->sectionActivated()
) | rpl::on_next([=](int index) {
boostsWrap->toggle(!index, anim::type::instant);
giftsWrap->toggle(index, anim::type::instant);
}, inner->lifetime());
Statistics::AddBoostsList(
status.firstSliceBoosts,
boostsWrap->entity(),
boostClicked,
_peer,
tr::lng_boosts_title());
Statistics::AddBoostsList(
status.firstSliceGifts,
giftsWrap->entity(),
std::move(boostClicked),
_peer,
tr::lng_boosts_title());
Ui::AddSkip(inner);
Ui::AddSkip(inner);
Ui::AddDividerText(inner, status.overview.group
? tr::lng_boosts_list_subtext_group()
: tr::lng_boosts_list_subtext());
}
Ui::AddSkip(inner);
Ui::AddSkip(inner);
AddHeader(inner, tr::lng_boosts_link_title);
Ui::AddSkip(inner, st::boostsLinkSkip);
FillShareLink(inner, _show, status.link, _peer);
Ui::AddSkip(inner);
Ui::AddDividerText(inner, status.overview.group
? tr::lng_boosts_link_subtext_group()
: tr::lng_boosts_link_subtext());
FillGetBoostsButton(inner, _controller, _show, _peer, reloadOnDone);
resizeToWidth(width());
crl::on_main(this, [=]{ fakeShowed->fire({}); });
}
void InnerWidget::saveState(not_null<Memento*> memento) {
memento->setState(base::take(_state));
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_state = memento->state();
if (!_state.link.isEmpty()) {
fill();
} else {
load();
}
Ui::RpWidget::resizeToWidth(width());
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
auto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {
return _showRequests.events();
}
void InnerWidget::showFinished() {
_showFinished.fire({});
}
not_null<PeerData*> InnerWidget::peer() const {
return _peer;
}
} // namespace Info::Boosts

View File

@@ -0,0 +1,63 @@
/*
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 "data/data_boosts.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/vertical_layout.h"
namespace Ui {
class Show;
} // namespace Ui
namespace Info {
class Controller;
} // namespace Info
namespace Info::Boosts {
class Memento;
class InnerWidget final : public Ui::VerticalLayout {
public:
struct ShowRequest final {
};
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PeerData*> peer);
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;
void showFinished();
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
private:
void load();
void fill();
not_null<Controller*> _controller;
not_null<PeerData*> _peer;
std::shared_ptr<Ui::Show> _show;
Data::BoostStatus _state;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<ShowRequest> _showRequests;
rpl::event_stream<> _showFinished;
rpl::event_stream<bool> _loaded;
};
} // namespace Info::Boosts

View File

@@ -0,0 +1,118 @@
/*
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/channel_statistics/boosts/info_boosts_widget.h"
#include "info/channel_statistics/boosts/info_boosts_inner_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
#include "ui/ui_utility.h"
namespace Info::Boosts {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(controller->statisticsTag()) {
}
Memento::Memento(not_null<PeerData*> peer)
: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) {
}
Memento::~Memento() = default;
Section Memento::section() const {
return Section(Section::Type::Boosts);
}
void Memento::setState(SavedState state) {
_state = std::move(state);
}
Memento::SavedState Memento::state() {
return base::take(_state);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _inner(setInnerWidget(
object_ptr<InnerWidget>(
this,
controller,
controller->statisticsTag().peer))) {
_inner->showRequests(
) | rpl::on_next([=](InnerWidget::ShowRequest request) {
}, _inner->lifetime());
_inner->scrollToRequests(
) | rpl::on_next([=](const Ui::ScrollToRequest &request) {
scrollTo(request);
}, _inner->lifetime());
}
not_null<PeerData*> Widget::peer() const {
return _inner->peer();
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
return (memento->statisticsTag().peer == peer());
}
rpl::producer<QString> Widget::title() {
return tr::lng_boosts_title();
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
rpl::producer<bool> Widget::desiredShadowVisibility() const {
return rpl::single<bool>(true);
}
void Widget::showFinished() {
_inner->showFinished();
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
return std::make_shared<Info::Memento>(
std::vector<std::shared_ptr<ContentMemento>>(
1,
std::make_shared<Memento>(peer)));
}
} // namespace Info::Boosts

View File

@@ -0,0 +1,67 @@
/*
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 "data/data_boosts.h"
#include "info/info_content_widget.h"
namespace Info::Boosts {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
using SavedState = Data::BoostStatus;
void setState(SavedState states);
[[nodiscard]] SavedState state();
private:
SavedState _state;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(not_null<ContentMemento*> memento) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredShadowVisibility() const override;
void showFinished() override;
[[nodiscard]] not_null<PeerData*> peer() const;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
const not_null<InnerWidget*> _inner;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
} // namespace Info::Boosts