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

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